100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Batch Apex

Learn how to process millions of records safely using the Database.Batchable interface, controlling chunk size, state, and job chaining.

Advanced ApexIntermediate9 min readJul 10, 2026
Analogies

Why Batch Apex Exists

Batch Apex is designed to process large numbers of records — from thousands to millions — asynchronously by breaking them into manageable chunks, each executed as its own transaction with its own fresh governor limits. You implement the Database.Batchable<sObject> interface with three required methods: start(), which defines the record set via a QueryLocator or iterable; execute(), which processes each chunk (scope); and finish(), which runs after all chunks complete, often used for summary emails or chaining another job.

🏏

Cricket analogy: It's like a groundstaff team mowing a full Test match outfield in strips rather than trying to cut the entire ground in one uninterrupted pass — each strip is its own manageable unit of work.

Implementing Database.Batchable

The start method typically returns a Database.QueryLocator built from a SOQL query, which can handle up to 50 million records because it bypasses the normal 50,000-record SOQL governor limit; alternatively it can return an Iterable<sObject> for custom logic, but that path is still bound by the standard heap and query limits. The execute method receives a Database.BatchableContext and a List<sObject> representing the current chunk (scope), and each invocation of execute is its own transaction — meaning a DML failure in one batch doesn't automatically roll back records already committed in previous batches unless you explicitly handle it.

🏏

Cricket analogy: It's like a Test match being split into separate sessions, where a rain delay in the afternoon session doesn't erase the runs already scored in the morning session — each session stands on its own.

apex
public class RecalculateAccountScoreBatch implements Database.Batchable<sObject>, Database.Stateful {
    public Integer recordsProcessed = 0;

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, AnnualRevenue, NumberOfEmployees FROM Account WHERE Score__c = null'
        );
    }

    public void execute(Database.BatchableContext bc, List<Account> scope) {
        for (Account acc : scope) {
            acc.Score__c = ScoringEngine.calculate(acc.AnnualRevenue, acc.NumberOfEmployees);
        }
        update scope;
        recordsProcessed += scope.size();
    }

    public void finish(Database.BatchableContext bc) {
        System.debug('Total accounts scored: ' + recordsProcessed);
    }
}

Batch Size, Stateful Interface, and Chaining

Database.executeBatch(new RecalculateAccountScoreBatch(), 200) lets you control the scope size, defaulting to 200 records per execute call but reducible to as low as 1 if each record's processing is callout-heavy or DML-intensive. By default, instance variables reset between chunks because each execute is a separate transaction; implementing Database.Stateful preserves instance member values (like a running total) across all chunks, though it does not preserve collection contents beyond what you deliberately maintain. Batch jobs are limited to 5 concurrent active or queued batch jobs per org, and you can chain a new job from within finish() to build multi-stage pipelines.

🏏

Cricket analogy: It's like a captain adjusting field settings between overs — a spinner might bowl to a packed off-side field one over and a defensive field the next, tuning the setup to the situation just like adjusting batch scope size.

A batch job with a badly filtered start() query — for example scanning an entire multi-million-row object without a selective WHERE clause — can trigger a QueryLocator timeout or excessive query optimizer cost; always index and filter your start query.

Testing and Monitoring Batch Jobs

Test classes for batch Apex must wrap the executeBatch call between Test.startTest() and Test.stopTest(), which forces the batch to run synchronously in a single chunk for assertion purposes, regardless of the scope size you pass. In production, you monitor progress by querying AsyncApexJob for fields like JobItemsProcessed, TotalJobItems, and NumberOfErrors, or by viewing Setup > Apex Jobs, and you can schedule automatic retries or alerting based on the finish() method detecting a nonzero error count.

🏏

Cricket analogy: It's like a dress-rehearsal warm-up match before the real tournament — Test.startTest()/stopTest() lets you see the batch run through its full innings in a controlled, compressed setting before it faces live conditions.

Batch Apex counts toward the 250,000 daily asynchronous Apex method executions limit per org (shared with future, queueable, and scheduled jobs), so high-volume nightly batches should be sized carefully.

  • Implement Database.Batchable<sObject> with start(), execute(), and finish() methods to process large record sets.
  • start() returns a Database.QueryLocator (up to 50 million records) or an Iterable<sObject>.
  • Each execute() call on a chunk (scope) is its own transaction with its own governor limits.
  • Control chunk size via the second argument to Database.executeBatch(), defaulting to 200.
  • Implement Database.Stateful to preserve instance variable values across chunks.
  • Orgs allow at most 5 concurrent active/queued batch jobs; chain new jobs from finish().
  • Test.startTest()/Test.stopTest() forces batch execution synchronously for unit tests.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ApexSalesforceStudyNotes#BatchApex#Batch#Apex#Exists#Implementing#StudyNotes#SkillVeris#ExamPrep