When we work in Salesforce, Apex code runs with certain limits to protect system performance. These limits are usually not a problem when we handle a small number of records.
However, when a requirement involves processing thousands or millions of records at the same time, normal Apex code may fail due to limits such as CPU execution time, heap size, SOQL query limits, or DML limits.
Batch Apex is used in such situations. It allows Salesforce to process large amounts of data by splitting records into smaller groups and processing them one group at a time in the background, where each batch gets fresh governor limits.
Batch Apex is a Salesforce mechanism used to process large volumes of records in a controlled and scalable way. Instead of processing all records in one transaction, Batch Apex breaks the work into smaller pieces and processes them one by one in the background. This approach prevents governor limit issues and ensures stable system performance.
In normal Apex execution:
Batch Apex solves this by:
Database.Batchable is a Salesforce-provided interface that tells the platform: "This class is designed to process data in batches." When a class implements Database.Batchable, Salesforce automatically splits records, controls transaction boundaries, and manages retries.
Why implement Database.Batchable<SObject>? We are telling Salesforce: this class works on records, Salesforce should call start/execute/finish, and handle batch execution automatically. Without it, the job cannot run as a batch.
The interface requires three methods: start(), execute(), finish() — Salesforce controls the execution order.
A scope is a small group of records passed to execute(). Example: 10,000 records, batch size 200 → 50 execute calls. Each scope runs independently, own transaction, fresh limits.
Every batch class is built on three mandatory methods: start, execute, finish. Salesforce controls when they are called.
Returns a Database.QueryLocator (most common, up to 50M records) or Iterable<SObject> for custom logic. Runs only once.
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id FROM Opportunity WHERE StageName = \'Closed Won\''
);
}
Called once per batch, receives a scope list. DML, calculations, callouts (if enabled) happen here.
global void execute(Database.BatchableContext bc, List<Opportunity> scope) {
for (Opportunity opp : scope) opp.Last_Processed_Date__c = Date.today();
Database.update(scope, false);
}
Runs once after all batches: send emails, log summary, chain another batch.
global void finish(Database.BatchableContext bc) {
System.debug('batch done');
}
By default, each execute gets a new instance — variables reset. Database.Stateful preserves instance variables across batches via serialization.
global class StatefulBatch implements Database.Batchable<Account>, Database.Stateful {
global Integer totalProcessed = 0;
global void execute(... , List<Account> scope) { totalProcessed += scope.size(); }
global void finish(...) { System.debug('total ' + totalProcessed); }
}
Preserved: instance variables (primitives, collections, wrappers). Not preserved: local vars, static vars, transaction state.
By default callouts are disabled. Implement Database.AllowsCallouts to enable HTTP in execute().
global class CalloutBatch implements Database.Batchable<Account>, Database.AllowsCallouts {
global void execute(... , List<Account> scope) {
Http http = new Http(); HttpRequest req = ...;
}
}
Use Schedulable interface and System.schedule with CRON.
global class ClosedWonBatchScheduler implements Schedulable {
global void execute(SchedulableContext sc) {
Database.executeBatch(new UpdateClosedWonOppsBatch(), 200);
}
}
// schedule: System.schedule('daily', '0 0 3 * * ?', new ClosedWonBatchScheduler());
From LWC, Apex class, flow, or custom button via Database.executeBatch(new YourBatch(), batchSize);
Start another batch inside finish(): Database.executeBatch(new NextBatchJob(), 200);
Max 5 active + 100 queued jobs. Salesforce automatically manages overflow.
Use partial DML: Database.update(scope, false); and inspect SaveResult to log failures without stopping the batch.
Batch is asynchronous, so tests need Test.startTest() and Test.stopTest() to force synchronous execution.
@IsTest static void testBatch() {
// insert test data
Test.startTest();
Database.executeBatch(new UpdateClosedWonOppsBatch(), 200);
Test.stopTest();
// assertions after batch completes
}
Requirement: update Last_Processed_Date__c on all Closed Won Opportunities.
global class UpdateClosedWonOppsBatch implements Database.Batchable<Opportunity> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Last_Processed_Date__c FROM Opportunity WHERE StageName = \'Closed Won\''
);
}
global void execute(Database.BatchableContext bc, List<Opportunity> scope) {
for (Opportunity opp : scope) opp.Last_Processed_Date__c = Date.today();
Database.update(scope, false);
}
global void finish(Database.BatchableContext bc) {
System.debug('Closed Won Opportunities processed.');
}
}
Batch Apex is Salesforce's solution for processing large data volumes safely. It splits work into smaller chunks, each with fresh limits.
Test.startTest/stopTestEssential for enterprise background processing, data maintenance, and large‑scale automation.