Batch Apex is the standard way to process large data volumes on Salesforce. The structure is simple (three methods, one interface) but the design choices matter: scope size, state preservation, error handling, and chaining all need deliberate decisions. Build and tune in a sandbox with realistic data volumes before running in production.
- Confirm Batch Apex is the right tool
Use Batch Apex when you need to process more records than a synchronous transaction can handle. For smaller async work with complex data, Queueable Apex is simpler. For cron-based scheduling, use Schedulable Apex that delegates to a batch job for the actual work.
- Create the class with Database.Batchable interface
public with sharing class MyBatchJob implements Database.Batchable<sObject> { ... }. Implement start, execute, and finish methods. Use sObject as the generic type for most jobs; use a custom type for non-SObject iterables.
- Implement start to return the records to process
Return a Database.QueryLocator for SOQL-based selection: return Database.getQueryLocator(''SELECT Id, Name FROM Account WHERE Stale__c = true''). QueryLocator supports up to 50 million records. For custom data sources, return an Iterable<sObject> instead, which has lower limits.
- Implement execute with per-chunk business logic
Receive List<sObject> records as the second parameter. Process them with normal Apex logic: SOQL queries, DML operations, callouts (if Database.AllowsCallouts is also implemented). Each chunk has its own governor limits, so bulkify carefully within the chunk.
- Implement finish for post-job cleanup
Send completion notifications, log job summary to a custom Error_Log__c, or chain the next batch job via Database.executeBatch. Use the Database.BatchableContext parameter to get the job ID for queries against AsyncApexJob.
- Add Database.Stateful if aggregation is needed
Implements Database.Batchable<sObject>, Database.Stateful. Instance variables preserved across chunks. Use for running totals, accumulated error lists, or any state that must survive from execute to finish.
- Write a test class with realistic data volumes
@isTest class with test methods that build 200+ records and call Database.executeBatch from inside Test.startTest/Test.stopTest. Confirm execute fires the right number of times and finish runs once. Test failures, retries, and edge cases.
- Schedule or invoke and monitor the job
Run from Anonymous Apex: Database.executeBatch(new MyBatchJob(), 200). Schedule via Schedulable Apex for recurring execution. Monitor Setup > Apex Jobs for status. Build a dashboard against AsyncApexJob for production visibility.
Records per execute invocation. Default 200, up to 2,000 for QueryLocator jobs. Tune based on per-record work and governor limit headroom.
Optional interface for preserving instance state across chunks. Required for cross-chunk aggregation and accumulated results.
Optional interface that lets execute methods make HTTP callouts to external systems. Required for jobs that need external integration.
- QueryLocator supports up to 50 million records, but Iterable scope is much smaller. If the data source is not directly queryable via SOQL, profile the iterable performance before assuming it scales.
- Each chunk runs in its own transaction. Instance variables on the batch class do not preserve across chunks unless Database.Stateful is implemented. Forgetting this is the leading cause of mysterious zero-totals at finish.
- Async governor limits apply across the whole job (cumulative callouts, total CPU, future method calls). Long-running jobs can still hit these even though per-chunk limits reset.
- Failed chunks roll back their DML but do not stop the job. The job continues processing remaining chunks. Build error tracking into execute (try/catch with Error_Log__c) because the default behavior silently swallows partial failures.
- Chained batch jobs from finish can produce infinite loops if termination criteria are wrong. Always include explicit stop conditions and monitor AsyncApexJob for runaway chains in production.