Hey folks, Abhi here!
So you’ve already gotten comfortable with Batch Apex. You know it processes millions of records in chunks, you know each chunk gets a fresh set of governor limits, and you know it’s your go-to for heavy data jobs. Great.
But then a requirement lands on your desk that a single batch just can’t handle cleanly:
“Clean up all the Accounts first, then recalculate Contact rollups, then send a summary email once everything is done.”
Three steps. Three different objects. Each one depends on the previous one finishing. You can’t just fire off three Database.executeBatch() calls and hope they run in order β because they won’t. They’ll all hit the Flex Queue and run whenever Salesforce feels like it.
This is exactly where batch chaining comes in. Let’s break down why you need it, when you actually need it, and how to wire it up without shooting yourself in the foot.
π€ Why Can’t I Just Run Multiple Batches?
The instinct is understandable. You’ve got three jobs, so you write:
apex
Database.executeBatch(new AccountCleanupBatch(), 200);Database.executeBatch(new ContactRollupBatch(), 200);Database.executeBatch(new SummaryEmailBatch(), 200);
Looks reasonable. It’s also wrong.
The moment you call executeBatch, the job goes into the Apex Flex Queue and runs as a discrete, asynchronous transaction. Salesforce doesn’t guarantee order. Worse, you can only have 5 concurrent batch jobs running at a time. So your “sequence” might run out of order, run in parallel, or get queued in a way that completely breaks your dependency chain.
If Step 2 depends on Step 1 having finished, parallel execution is a recipe for half-baked data and angry users.
The whole point of chaining is to enforce sequence: Job B only starts after Job A is completely done.
βοΈ What Is Batch Chaining, Really?
Batch chaining means starting the next batch from inside the finish() method of the current one.
Remember the three methods of a batch class:
start()β collects the recordsexecute()β processes each chunkfinish()β runs once, after every chunk has completed
That finish() method is the magic. It only fires when the entire batch is done. So if you kick off the next job from there, you get a guaranteed sequential handoff.
apex
global class AccountCleanupBatch implements Database.Batchable<SObject> { global Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator( 'SELECT Id, Name FROM Account WHERE NeedsCleanup__c = true' ); } global void execute(Database.BatchableContext bc, List<Account> scope) { for (Account acc : scope) { acc.Name = acc.Name.trim(); } update scope; } global void finish(Database.BatchableContext bc) { // Chain the next job ONLY after this one fully completes Database.executeBatch(new ContactRollupBatch(), 200); }}
When AccountCleanupBatch is 100% finished, finish() runs and fires off ContactRollupBatch. Clean, sequential, predictable.
π― When Do You Actually Need to Chain?
Don’t chain just because you can. Chaining adds complexity, so reach for it only when you genuinely need ordered, multi-step processing. Here are the real-world cases I keep running into:
1. Multi-object data migrations. You migrate Accounts first, then Contacts that reference those Accounts, then Opportunities. Order matters because of lookups and relationships.
2. Dependent calculations. Job A updates a field, Job B reads that updated field to compute a rollup. If B runs before A finishes, your numbers are garbage.
3. Staged cleanup jobs. Deduplicate first, then merge, then archive. Each phase has to complete before the next begins.
4. Reprocessing pipelines. Pull failed integration records, transform them, then push them back out β each stage as its own batch.
5. Processing more than you can hold in one job. Sometimes a single batch can’t logically cover everything, and you split the work into sequenced stages.
If your steps are independent and order doesn’t matter, don’t chain β just run them separately and let Salesforce parallelize.
π§± A Clean Chaining Pattern
Here’s the thing β hard-coding new NextBatch() inside every finish() works, but it gets ugly fast. Your jobs become tightly coupled, and you can’t test or run a single job in isolation without editing code.
A cleaner approach is to pass the “next job” into the constructor, so each batch doesn’t need to know hard-coded what comes after it:
apex
global class ChainableBatch implements Database.Batchable<SObject> { private Database.Batchable<SObject> nextJob; // Pass in whatever should run next (or null to stop) global ChainableBatch(Database.Batchable<SObject> nextJob) { this.nextJob = nextJob; } global Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator('SELECT Id FROM Account'); } global void execute(Database.BatchableContext bc, List<SObject> scope) { // your processing logic } global void finish(Database.BatchableContext bc) { if (nextJob != null) { Database.executeBatch(nextJob, 200); } }}
Now your chaining logic lives in one place and each job stays decoupled. You can run a single job by passing null, or wire up a full chain when you need it. This is the same idea behind the reusable “ChainableBatchJob” patterns floating around the community β decouple the orchestration from the individual jobs.
β οΈ Things That’ll Bite You
A few gotchas worth knowing before you ship this to production.
Don’t chain a batch into itself recursively. If your finish() re-runs the same batch unconditionally, you’ve built an infinite loop that’ll chew through your async limits. If you need a job to run on a schedule, schedule it with System.schedule() instead of self-chaining.
Watch your Flex Queue. You can hold up to 100 jobs in the Apex Flex Queue and run 5 concurrently. Chaining is friendly here because each chain only adds one job at a time, but if other processes are flooding the queue, your chained job still has to wait its turn.
You can’t call @future from a batch. A common reflex is to fire a future method from inside execute(). Salesforce blocks this to prevent runaway async chains. If you need async work mid-batch, use Queueable Apex instead β you can call a Queueable from a batch.
Add error handling in the chain. If Step 1 fails, do you really want Step 2 to run on bad data? Use Database.Stateful to track failures and decide in finish() whether to continue the chain or stop and send an alert.
π Batch Chaining vs Queueable Chaining
Quick reality check before you commit to Batch chaining. If your jobs don’t need to process millions of records, Queueable Apex might be the better tool. Queueable natively supports chaining via System.enqueueJob(), handles complex object parameters, and is generally the modern recommendation for multi-step async work.
A rough rule I follow:
| Situation | Best Tool |
|---|---|
| Millions of records per step | Batch Apex (chained) |
| Multi-step logic, smaller volumes | Queueable Apex |
| Need to pass complex objects between steps | Queueable Apex |
| Simple fire-and-forget callout | Future method |
If you’ve got fewer than a couple batches’ worth of records, you’re probably better off with Queueable. Batch chaining shines when each individual step genuinely needs the muscle of Batch Apex.
π Key Takeaways
- Multiple
executeBatch()calls don’t run in order β they hit the Flex Queue and run whenever. - Chain jobs by calling
Database.executeBatch()inside thefinish()method, which only runs after the full batch completes. - Chain only when steps are genuinely dependent β migrations, dependent calculations, staged pipelines.
- Decouple your chaining logic instead of hard-coding the next job everywhere.
- Never self-chain recursively β schedule instead.
- You can’t call
@futurefrom a batch, but you can call Queueable. - For smaller, multi-step work, Queueable chaining is often the cleaner choice.
The biggest lesson? Chaining isn’t about forcing more batches to run β it’s about controlling when they run. Get the sequencing right, and your multi-step jobs become predictable instead of a race condition waiting to happen.
Catch you in the next one!
β Abhi
Leave a comment