Queueable can chain itself, and a Batch can start a Queueable. However, a @future method is a dead end—it cannot call another @future, and a Batch cannot call a @future. Memorizing this simple matrix eliminates an entire category of runtime crashes.
Key Points at a Glance
- The Future is a Dead End: You cannot call a
@futuremethod from any other async context (Batch, Queueable, or another Future). - Queueable is the Swiss Army Knife: You can enqueue a Queueable from a Trigger, a Batch
execute()orfinish(), and even chain it to another Queueable. - Batch Chaining: You can legally chain a new Batch job, but only from the
finish()method of the current Batch. - Callout Rule: Callouts require the correct marker (
callout=trueorDatabase.AllowsCallouts) and can never occur after a DML operation in the same transaction.
Imagine a Batch job finishes processing and needs to notify an external system.
Wrong approach: Calling a
@future method from the finish() block. This is illegal and will crash.Right approach: Enqueueing a
Queueable job from the finish() block. This is completely legal and represents the Salesforce-approved way to chain async work.
The Async "Can I Call X from Y" Matrix
The rules of async composition can trip up even veteran developers, and system architects love testing this knowledge in interviews. There is no underlying mathematical principle to derive these rules from—you just have to memorize them.
| From ↓ \ Call → | @future | Queueable | Batch | Scheduled |
|---|---|---|---|---|
| Trigger / Sync | Yes (≤50) | Yes (≤50) | Yes | Yes |
| @future | ❌ No | ❌ No | ❌ No | ❌ No |
| Queueable | ❌ No | ✅ Yes (Chain) | ✅ Yes | ✅ Yes |
| Batch execute() | ❌ No | ✅ Yes | ❌ No | — |
| Batch finish() | ❌ No | ✅ Yes | ✅ Yes (Chain) | ✅ Yes |
| Scheduled | ❌ No | ✅ Yes | ✅ Yes | — |
@future is a DEAD END (you can't chain it or call it from async). Queueable is your SWISS ARMY KNIFE (callable from triggers and batches, can chain itself, handles callouts). Batch chains exclusively from finish().
- Rule:
@futureis restrictive. Queueable is for general-purpose heavy lifting. Batch chains from the finish method. - Gain: Knowing the matrix allows you to design a legal integration architecture upfront, rather than dealing with runtime exceptions in production.
- Reach For: Use Queueable for almost everything. Use Batch when you need the platform to chunk millions of records automatically. Use Scheduled as a lightweight launcher. Only write a new
@futuremethod if you need to bypass a mixed-DML error (Setup vs. Non-Setup objects). - Volume Alert: If you have 5 million records requiring a callout each, run a Batch with a scope size of 100. This ensures each
execute()chunk respects the 100-callout per transaction limit.
@future method? NO. The platform strictly blocks async-inside-async patterns that could trigger infinite, runaway fan-outs. Learn which combinations are blocked to avoid designing flawed architectures.
Core Q&A and Common Scenarios
Database.AllowsCallouts to make callouts inside execute(). Then, use finish() to enqueue the next Batch or Queueable."
A: Break it down into two parts: the callout and the chain.
- The Callout: Mark the batch class with
Database.AllowsCallouts. Eachexecute()chunk gets fresh limits. However, if the callout must happen after a DML operation, or if you need isolated retries, you should enqueue aQueueablefrom withinexecute(). This is perfectly legal. - The Chain: In the
finish()method, callDatabase.executeBatch(new NextBatch()). Chaining a batch fromfinish()is permitted.
Calling a @future method from anywhere in this process is strictly prohibited.
// 1. A batch job allowed to make callouts.
global class SyncBatch implements Database.Batchable<sObject>, Database.AllowsCallouts {
// 2. start() decides WHICH records process.
global Iterable<sObject> start(Database.BatchableContext bc) { /* ... */ }
// 3. execute() runs once per chunk, with fresh limits.
global void execute(Database.BatchableContext bc, List<sObject> scope) {
// You can make a callout here directly, OR enqueue a Queueable for isolation:
// Enqueueing from a batch is allowed. @future is NOT.
System.enqueueJob(new CalloutJob(scope));
}
global void finish(Database.BatchableContext bc) {
// 4. Chaining the next batch from finish() is completely legal.
Database.executeBatch(new NextBatch());
}
}
A: It fails because Salesforce blocks asynchronous methods from calling @future methods to prevent infinite loop explosions. Tell the developer to replace the @future method with a Queueable class.
Queueables support Database.AllowsCallouts, can be fired from a batch execute() method, and can carry complex data states (like sObjects). Furthermore, they support Finalizers for guaranteed error logging. "Replace @future with Queueable" is almost always the correct modernization advice.
A: Use Batch Apex with a custom scope size of 100. Because every execute() chunk gets its own set of fresh transaction limits, a scope of 100 guarantees you will never exceed the 100-callout limit.
If you need granular retries or if the callout has to happen after a database insert, fire a Queueable from the execute() method. Just ensure your batch scope stays under the 50-enqueue-per-transaction limit (e.g., set the batch scope to 50). Add a Finalizer to the Queueable to capture any HTTP failures in a custom retry log object.
๐ 2-Minute Self-Check
- Q: A transaction hits a CPU timeout. What do you check first?
A: Check the entire automation stack. CPU limits usually break due to a cascading chain of triggers and overlapping flows, not just one bad method. - Q: You need to make a callout right after updating a record, but it fails. Why?
A: You cannot perform an HTTP callout after a DML statement in the exact same transaction (it creates uncommitted work pending errors). Move the callout to a Queueable or publish a Platform Event. - Q: A colleague suggests using @future for a brand-new integration job. Your answer?
A: Say no. Use Queueable. It accepts complex sObjects as parameters, returns a Job ID you can monitor, can be chained, and supports Finalizers for robust error handling.