Skip to main content

SF-0381 · Coding · Medium

Can you write a sample batch apex with Database.Stateful?

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

Here’s a realistic Batch class with Database.Stateful — it sums opportunity amounts per account across millions of records and writes the rollups in finish.

The class

public class OpptyRollupBatch implements
    Database.Batchable<sObject>,
    Database.Stateful
{
    // State surviving across chunks — possible because of Database.Stateful
    public Map<Id, Decimal> totalByAccount = new Map<Id, Decimal>();
    public Map<Id, Integer> countByAccount = new Map<Id, Integer>();
    public Integer recordsProcessed = 0;
    public Integer errors = 0;
    public List<String> errorList = new List<String>();

    public Database.QueryLocator start(Database.BatchableContext ctx) {
        return Database.getQueryLocator(
            'SELECT Id, AccountId, Amount, StageName ' +
            'FROM Opportunity ' +
            'WHERE IsClosed = false AND AccountId != null'
        );
    }

    public void execute(Database.BatchableContext ctx, List<Opportunity> scope) {
        for (Opportunity o : scope) {
            try {
                Decimal amt = (o.Amount == null) ? 0 : o.Amount;

                // Sum amounts per account, across chunks
                if (!totalByAccount.containsKey(o.AccountId)) {
                    totalByAccount.put(o.AccountId, 0);
                    countByAccount.put(o.AccountId, 0);
                }
                totalByAccount.put(o.AccountId, totalByAccount.get(o.AccountId) + amt);
                countByAccount.put(o.AccountId, countByAccount.get(o.AccountId) + 1);
                recordsProcessed++;
            } catch (Exception e) {
                errors++;
                errorList.add('Opp ' + o.Id + ': ' + e.getMessage());
            }
        }
    }

    public void finish(Database.BatchableContext ctx) {
        // Write the rollups in bulk — totalByAccount has *all* accounts processed
        List<Account> toUpdate = new List<Account>();
        for (Id accId : totalByAccount.keySet()) {
            toUpdate.add(new Account(
                Id = accId,
                Open_Opp_Total__c = totalByAccount.get(accId),
                Open_Opp_Count__c = countByAccount.get(accId)
            ));
        }

        // Chunk the update if we have >10K (DML row limit)
        Integer chunk = 1000;
        for (Integer i = 0; i < toUpdate.size(); i += chunk) {
            Integer end = Math.min(i + chunk, toUpdate.size());
            List<Account> slice = new List<Account>();
            for (Integer j = i; j < end; j++) slice.add(toUpdate[j]);
            Database.update(slice, false);
        }

        // Email summary
        AsyncApexJob job = [
            SELECT Status, CreatedBy.Email FROM AsyncApexJob WHERE Id = :ctx.getJobId()
        ];
        Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
        msg.setToAddresses(new String[] { job.CreatedBy.Email });
        msg.setSubject('Opp Rollup Batch: ' + job.Status);
        msg.setPlainTextBody(
            'Opps processed: ' + recordsProcessed + '\n' +
            'Accounts updated: ' + totalByAccount.size() + '\n' +
            'Errors: ' + errors
        );
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { msg });
    }
}

How to invoke

Database.executeBatch(new OpptyRollupBatch(), 200);

What makes this pattern strong

FeatureWhy it matters
Database.StatefultotalByAccount survives across chunks — essential for aggregation
Map keyed by AccountIdAllows the same account to appear across multiple chunks and still aggregate correctly
Single bulk update in finishWrites rollups in one efficient pass after all chunks complete
Chunked DML inside finishDefends against the 10K DML row limit if many accounts exist
Per-record try/catchFailures don’t crash the chunk
Email summaryStandard end-of-job notification

A simpler stateful pattern

If they want a minimal example:

public class TaggerBatch implements Database.Batchable<sObject>, Database.Stateful {
    public Integer total = 0;

    public Database.QueryLocator start(Database.BatchableContext ctx) {
        return Database.getQueryLocator('SELECT Id FROM Lead WHERE IsConverted = false');
    }
    public void execute(Database.BatchableContext ctx, List<Lead> scope) {
        for (Lead l : scope) l.Description = 'Tagged ' + Date.today();
        update scope;
        total += scope.size();
    }
    public void finish(Database.BatchableContext ctx) {
        System.debug('Total leads tagged: ' + total); // shows the actual cumulative total
    }
}

Verified against: Apex Developer Guide — Database.Stateful Interface. Last reviewed 2026-05-17.