Here’s a worked example: a batch that processes orders, tallies a running total of order amounts, collects every failed-record error, and emails a summary at the end. Everything that accumulates across chunks is an instance field that only works because of Database.Stateful.
The class
public class OrderTotalsBatch implements
Database.Batchable<sObject>,
Database.Stateful
{
// State that survives across executes — needs Database.Stateful
public Decimal totalRevenue = 0;
public Integer totalProcessed = 0;
public Integer totalErrors = 0;
public List<String> errorMessages = new List<String>();
public Datetime startTime;
public Database.QueryLocator start(Database.BatchableContext ctx) {
startTime = Datetime.now();
return Database.getQueryLocator(
'SELECT Id, Total__c, Status__c FROM Order__c WHERE Year__c = 2026'
);
}
public void execute(Database.BatchableContext ctx, List<Order__c> scope) {
for (Order__c o : scope) {
try {
// Accumulate revenue across chunks
totalRevenue += (o.Total__c == null ? 0 : o.Total__c);
// Update each order
o.Status__c = 'Reconciled';
totalProcessed++;
} catch (Exception e) {
totalErrors++;
errorMessages.add('Order ' + o.Id + ': ' + e.getMessage());
}
}
update scope;
}
public void finish(Database.BatchableContext ctx) {
Long duration = (Datetime.now().getTime() - startTime.getTime()) / 1000;
AsyncApexJob job = [
SELECT CreatedBy.Email FROM AsyncApexJob WHERE Id = :ctx.getJobId()
];
Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
msg.setToAddresses(new String[] { job.CreatedBy.Email });
msg.setSubject('Order Totals Batch Complete');
msg.setPlainTextBody(
'Orders processed: ' + totalProcessed + '\n' +
'Total revenue: $' + totalRevenue + '\n' +
'Errors: ' + totalErrors + '\n' +
'Duration: ' + duration + ' seconds\n\n' +
(totalErrors > 0
? 'First 10 errors:\n' + String.join(firstN(errorMessages, 10), '\n')
: 'No errors.')
);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { msg });
}
private List<String> firstN(List<String> list, Integer n) {
if (list.size() <= n) return list;
List<String> top = new List<String>();
for (Integer i = 0; i < n; i++) top.add(list[i]);
return top;
}
}
What’s stateful — and what’s not
| Field | Stateful? | Why |
|---|---|---|
totalRevenue | Yes (with interface) | Instance member, mutated across chunks |
totalProcessed, totalErrors | Yes | Same |
errorMessages | Yes | List grows across chunks |
startTime | Yes | Assigned once in start, read in finish |
Any static variable | No | Statics reset every transaction regardless |
scope parameter | N/A | Just the chunk’s records, lives only in execute |
The same class without Database.Stateful
// Without Database.Stateful
public class OrderTotalsBatch implements Database.Batchable<sObject> {
public Decimal totalRevenue = 0; // resets every execute!
// ...
}
After running, finish would show totalRevenue = 0 (or the last chunk’s value only) and errorMessages = []. Every chunk got a fresh instance, every increment was thrown away.
Other common stateful examples
- Per-account rollups —
Map<Id, Decimal>accumulating totals across chunks. - High-water marks —
oldestDate,largestAmountupdated as more records arrive. - Summary by category —
Map<String, Integer>counting record types. - Detailed per-record results — list of “succeeded”, “failed”, “skipped” rows for an end-of-job report.
Common interview follow-ups
- Are static variables stateful in batch? — No. Statics reset every transaction. Only instance fields plus
Database.Statefulwork. - Performance impact? — Slight. Salesforce serializes the instance between chunks. Don’t add it if you don’t need it.
- Can I store anything? — Anything serializable — sObjects, custom Apex types, primitives, maps, lists. Avoid transient fields.
Verified against: Apex Developer Guide — Database.Stateful Interface. Last reviewed 2026-05-17.