Skip to main content

SF-0378 · Concept · Medium

What is "state" in batch apex? Or What is Database.Stateful?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

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

NeedAdd Database.Stateful?
Running total of records processedYes
List of records that failed (for an end-of-job email)Yes
Map keyed by parent Id, aggregating child rollups across chunksYes
Knowing the start time so finish can compute durationYes
Processing each chunk independently with no cross-chunk awarenessNo

What it doesn’t do

  • Doesn’t preserve static variables. Statics reset per transaction regardless; Database.Stateful only 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 or AsyncApexJob extra 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.Stateful required for a chained batch? — No. Chaining means calling Database.executeBatch(new NextBatch()) in finish — the next job is a fresh instance regardless.
  • Are static variables stateful? — No. Statics live for a transaction; each execute is 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.