Batch Apex: One Brick at a Time — Async Apex Part 2
How Salesforce lets you process thousands of records without blowing up your Governor Limits — and why it thinks exactly like a Lego instruction manual.

In Part 1 we talked about why Async Apex exists — multi-tenancy, Governor Limits, and why going async gets you significantly more room to work. Now let's get into the first tool that actually uses it.
Batch Apex is the one you reach for when you have thousands — or even millions — of records to process. And the mental model that makes it click is simpler than you'd think.
Think Lego
When you open a Lego box, you don't get the finished model. You get hundreds of individual pieces and an instruction manual that walks you through building it chunk by chunk — a few bricks at a time. At the end, every piece comes together into the final thing.
Instead of throwing 50,000 records at Salesforce all at once — which would immediately hit Governor Limits — you break the work into smaller chunks and let Salesforce process them one batch at a time. Each chunk follows the same logic. The final result is the same as if you'd processed everything together.
Real example: You have 5,253 coupons and need to update all of them. Instead of processing 5,253 at once, you split them into batches of 200. Salesforce runs the logic on the first 200, then the next 200, and so on — until every record is processed. One brick at a time.
The Interface: Database.Batchable
Salesforce gives you an interface to structure your batch job — Database.Batchable. Implement this and you get three methods you must define.
1. start() — Pick your bricks
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM MyObject');
}
This is where you tell Salesforce which records to work on. It returns one of two things:
Database.QueryLocator— You hand Salesforce a SOQL query and it manages the rest. The normal 50,000 row limit doesn't apply here — you can query up to 50 million records.Iterable<sObject>— For when your data isn't a straightforward sObject. Think going to the library and coming back with a book, a magazine, and a DVD. Use this for custom wrapper classes or complex data structures.
Note: The
Database.BatchableContext bcparameter carries metadata about the running job — its ID, errors, notification info. Query it via theAsyncApexJobobject.
2. execute() — Build chunk by chunk
public void execute(Database.BatchableContext bc, List<SObject> scope) {
// Your business logic goes here
// scope = the current chunk of records
}
This is where the actual work happens. Every field update, every calculation, every business rule. Runs once per chunk with scope being the records in that batch.
3. finish() — Final piece snaps in
public void finish(Database.BatchableContext bc) {
// Runs once after all chunks are done
// Great for emails, cleanup, or chaining another job
}
Important: The order batches execute in is not guaranteed. Never write logic that assumes Batch 1 completes before Batch 2.
Live Example: Bulk Coupon Update
Scenario: Update every coupon in the org — prepend 2026_ to each coupon code and set the expiry to 30 days from today.
public with sharing class BatchCoupon_Generator
implements Database.Batchable<SObject>, Database.Stateful {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, EndDateTime, CouponCode FROM Coupon'
);
}
public void execute(Database.BatchableContext bc, List<Coupon> scope) {
List<Coupon> couponsToUpdate = new List<Coupon>();
for (Coupon cp : scope) {
cp.CouponCode = '2026_' + cp.CouponCode;
cp.EndDateTime = System.now().addDays(30);
couponsToUpdate.add(cp);
}
if (!couponsToUpdate.isEmpty()) {
update couponsToUpdate;
}
}
public void finish(Database.BatchableContext bc) {
System.debug('All coupons updated successfully.');
}
}
To run it from Anonymous Apex:
Database.executeBatch(new BatchCoupon_Generator(), 200);
The 200 is your batch size. You can go up to 2,000 but 200 is the sweet spot that keeps you safely within limits across all three methods.
Seeing It Run — Apex Jobs
Once you fire Database.executeBatch(), the job doesn't just disappear into the void. Salesforce queues it and you can watch it run in real time.
To find it:
Go to Setup → search "Apex Jobs" → Apex Jobs
You'll see a table with every batch job that's been queued, running, or completed. For your job specifically look out for:
Status —
Queued,Processing, orCompletedBatches Processed — how many chunks are done
Batches Total — total number of chunks
Failures — any chunks that errored out
Submitted Date — when you fired it
This is also where you'll catch errors if something goes wrong mid-batch — the failure count updates in real time per chunk, not just at the end.
Tip: If you want to monitor it programmatically instead of through the UI, query the
AsyncApexJobobject using the job ID returned byDatabase.executeBatch().
What is Database.Stateful?
By default each batch chunk runs in a completely fresh execution context — any instance variables set in one chunk are wiped before the next one starts.
When you add Database.Stateful, Salesforce preserves instance variables between chunks. Use it when you need to track something across the entire job — a running error count, a total of records updated, anything that needs continuity.
Rule of thumb: Use
Database.Statefulwhen you need continuity. Skip it when you don't — it comes with a small performance cost.
That's Batch Apex
You now have the full picture — why it exists, how the three methods work together, when to reach for Database.Stateful, and what a running job actually looks like in Apex Jobs.
Next time you're staring at a bulk operation that's too big to run synchronously, you know exactly what to do.
Break it down. One brick at a time.

