Skip to main content

Salesforce Async Apex Explained: The "Who Can Call Whom" Matrix

๐Ÿ’ฌ In plain words: Navigating asynchronous Apex is all about knowing who is allowed to call whom. A 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 @future method 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() or finish(), 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=true or Database.AllowsCallouts) and can never occur after a DML operation in the same transaction.
๐Ÿ“Œ Real-Life Example: The Batch Finish Trap
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
Salesforce Async Call Matrix
๐Ÿง  Dead End vs. Swiss Army Knife: @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().
๐Ÿงญ 360 Card — Master the Async Matrix
  • Rule: @future is 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 @future method 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.
⚠ INTERVIEW TRAP: Can a Batch job call a @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

Q: You have a Batch job. You need to make a callout for each record, and then kick off another Batch when you are done. How do you structure this legally?
๐ŸŽฏ Say this first: "Mark the batch with 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. Each execute() chunk gets fresh limits. However, if the callout must happen after a DML operation, or if you need isolated retries, you should enqueue a Queueable from within execute(). This is perfectly legal.
  • The Chain: In the finish() method, call Database.executeBatch(new NextBatch()). Chaining a batch from finish() 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());
  }
}
Q: A developer says: "I will just call a @future method from my batch to do the callout." Why does it fail, and what do you tell them?

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.

Q: You need to process 1 million records with a callout for each, while respecting the 100-callout-per-transaction limit. What's the architecture?

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.