Database.Stateful is an empty marker interface you add to a Batch class when you need member variables to survive across execute() calls. By default, each chunk gets a freshly constructed instance — fields reset to their default values. With Database.Stateful, the same instance carries forward, and accumulated state (counters, aggregations, lists of errors) persists from one chunk to the next and into finish().
The default: stateless
public class CleanupBatch implements Database.Batchable<sObject> {
private Integer totalProcessed = 0; // resets every execute
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator('SELECT Id FROM Case WHERE Status = \'New\'');
}
public void execute(Database.BatchableContext ctx, List<Case> scope) {
totalProcessed += scope.size(); // accumulates to 200, then resets
}
public void finish(Database.BatchableContext ctx) {
System.debug(totalProcessed); // shows 0 — final execute's value is gone too
}
}
After 10 chunks of 200 records, you’d expect totalProcessed = 2000. Without Database.Stateful, it’s 0 in finish — every chunk got a fresh instance, and the increments evaporated.
With Database.Stateful
public class CleanupBatch implements Database.Batchable<sObject>, Database.Stateful {
private Integer totalProcessed = 0;
private List<String> errorMessages = new List<String>();
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator('SELECT Id FROM Case WHERE Status = \'New\'');
}
public void execute(Database.BatchableContext ctx, List<Case> scope) {
try {
// ... do work
totalProcessed += scope.size();
} catch (Exception e) {
errorMessages.add('Chunk failed: ' + e.getMessage());
}
}
public void finish(Database.BatchableContext ctx) {
System.debug('Total: ' + totalProcessed); // correct cumulative total
System.debug('Errors: ' + errorMessages); // every error from every chunk
}
}
The same instance is reused, so the increments and appended errors carry through to finish.
When you need it
| Need | Add Database.Stateful? |
|---|---|
| Running total of records processed | Yes |
| List of records that failed (for an end-of-job email) | Yes |
| Map keyed by parent Id, aggregating child rollups across chunks | Yes |
Knowing the start time so finish can compute duration | Yes |
| Processing each chunk independently with no cross-chunk awareness | No |
What it doesn’t do
- Doesn’t preserve static variables. Statics reset per transaction regardless;
Database.Statefulonly affects instance fields. - Doesn’t make the job transactional. Failed chunks still roll back independently. The state just carries forward (the failed chunk’s increments are lost because the transaction rolled back).
- Doesn’t help across separate jobs. A second
Database.executeBatch(new MyBatch())call gets a fresh instance. Use a custom object orAsyncApexJobextra info if you need cross-job state.
Performance note
Database.Stateful has a small overhead — Salesforce has to serialize and deserialize the instance between chunks. If you don’t need it, don’t add it. For most batch jobs (pure transformations, no aggregation), stateless is correct and cheaper.
Common interview follow-ups
- Is
Database.Statefulrequired for a chained batch? — No. Chaining means callingDatabase.executeBatch(new NextBatch())infinish— the next job is a fresh instance regardless. - Are static variables stateful? — No. Statics live for a transaction; each
executeis a separate transaction. - Can I store complex Apex types? — Yes, anything serializable. Salesforce uses standard Apex serialization between chunks.
Verified against: Apex Developer Guide — Using Batch Apex (Database.Stateful). Last reviewed 2026-05-17.