Batch Apex

⚙️ What is Batch Apex in Salesforce?
Batch Apex is an asynchronous Apex feature used to process large volumes of records—in the range of thousands to millions—efficiently within governor limits.
✅ Why Use Batch Apex?
Salesforce enforces strict limits (SOQL, DML, heap size).
Batch Apex splits large jobs into manageable chunks (called batches).
Each batch runs in its own execution context with fresh governor limits.
🧠 Example Use Cases:
Data cleansing, recalculation, archiving, mass updates, integration logic, etc.
🔄 How It Works
A Batch Apex class implements the Database.Batchable
interface, which has 3 methods:
Method | Description |
start() | Collects the records to be processed (returns Database.QueryLocator or Iterable ) |
execute() | Processes a batch of records (default batch size: 200) |
finish() | Optional. Used for post-processing logic (like sending an email or chaining) |
🧮 Batch Apex Limits
Feature | Limit |
SOQL Rows Retrieved | Up to 50 million (Database.QueryLocator ) |
Default Batch Size | 200 records per chunk |
Max Simultaneous Batch Jobs | 5 active batch jobs at a time |
Max Records in Iterable | 50 million |
Max Batch Apex Invocations/Day | 250,000 or number of user licenses * 200, whichever is greater |
Heap Size per Transaction | 12 MB (vs 6 MB in synchronous Apex) |
⚠️ If a query returns more than 50 million records, the job fails immediately.
⚖️ Batch Apex vs Queueable Apex
Feature | Batch Apex | Queueable Apex |
Best for | Millions of records | Thousands of records |
Retry capability | Yes (automatic retry possible) | No built-in retry |
Chaining | Limited via finish() method | Easier using System.enqueueJob() |
Scope Size | Adjustable (default: 200) | Not chunked |
Governor Limits Reset | Per batch execution | Only once per job |
Complexity | Higher setup | Simpler to implement |
🧩 When to Use Batch Apex?
Use Batch Apex when:
Processing more than 50,000 records
You need error handling per batch
You want automatic retry on failed batches
You need to schedule processing over time
🚀 How to Declare a Batch Class in Salesforce
To declare a class as a Batch Apex class, it must implement the Database.Batchable<T>
interface, where T
is typically an sObject
type like Account
, Contact
, etc.
✅ Basic Syntax
public class BatchClass implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext info) {
String query = 'SELECT Id, Name FROM Account';
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext info, List<Account> accountList) {
// Perform logic on each batch of records
}
public void finish(Database.BatchableContext info) {
// Optional wrap-up logic, like sending emails
}
}
🧩 Purpose of Each Method
1. 🔍 start()
Purpose: Defines the records to be processed.
Runs once, when the batch job begins.
Returns: A
Database.QueryLocator
or anIterable<sObject>
.Max Records Allowed: 50 million with
QueryLocator
.
public Database.QueryLocator start(Database.BatchableContext info) {
return Database.getQueryLocator('SELECT Id, Name FROM Account');
}
2. ⚙️ execute()
Purpose: Processes each batch of records.
Runs multiple times — once for each batch.
Parameters:
Database.BatchableContext info
– contains job context.List<sObject> scope
– subset of records (default size 200).
public void execute(Database.BatchableContext info, List<Account> accountList) {
System.debug('List Size : ' + accountList.size());
}
3. ✅ finish()
Purpose: Final cleanup or post-job processing.
Called once after all batches finish.
You can chain another batch from here or send notifications.
public void finish(Database.BatchableContext info) {
System.debug('Job Id: ' + info.getJobId());
Database.executeBatch(new OtherBatchClass()); // Chaining another batch
}
▶️ How to Execute a Batch Class
You can execute a batch from Apex code using:
Database.executeBatch(new BatchClass(), 1000); // Batch size is optional (default: 200)
Minimum batch size: 1
Maximum batch size: 2000
Returns: Job ID (
AsyncApexJob.Id
) – used to monitor the job
Id jobId = Database.executeBatch(new BatchClass(), 500);
System.debug('Job ID: ' + jobId);
🔎 Getting Job and Chunk IDs
🌐 Inside All Methods:
info.getJobId(); // Returns 18-digit job ID of the batch
📦 Inside execute()
Only:
info.getChildJobId(); // Returns ID of the batch chunk job
❗
getChildJobId()
returnsnull
if used instart()
orfinish()
.
🔄 What is Apex Flex Queue?
The Apex Flex Queue allows you to submit up to 100 batch jobs for future execution beyond the 5 active/queued job limit.
Standard Limit: Salesforce allows only 5 batch jobs to be active/queued at any time.
Flex Queue: Additional 100 jobs can be submitted and held in
Holding
state.Jobs are processed FIFO (First In, First Out) unless manually reordered.
⚠️ If you submit more than 100 holding jobs, you’ll get:
System.AsyncException: You’ve exceeded the limit of 100 jobs in the flex queue for org [ID].
📊 Apex Flex Queue Job Statuses
Status | Description |
Holding | Job submitted but waiting in flex queue. |
Queued | Job moved from holding to ready-to-execute state. |
Preparing | Start method is running; data is being loaded. |
Processing | Job is currently executing (inside execute method). |
Completed | Job finished successfully (some failures may be allowed). |
Failed | Job failed due to system error or unhandled exception. |
Aborted | Job was canceled manually by a user. |
⚙️ Controlling the Flex Queue
You can reorder jobs in the Flex Queue programmatically using the System.FlexQueue
class.
✅ Common Methods
1. 🔁 moveBeforeJob(jobToMoveId, jobInQueueId)
Moves jobToMoveId
before jobInQueueId
.
System.FlexQueue.moveBeforeJob('707xx0000000001', '707xx0000000002');
2. 🔁 moveAfterJob(jobToMoveId, jobInQueueId)
Moves jobToMoveId
after jobInQueueId
.
System.FlexQueue.moveAfterJob('707xx0000000001', '707xx0000000002');
3. ⬇️ moveJobToEnd(jobId)
Moves a job to the end of the flex queue.
System.FlexQueue.moveJobToEnd('707xx0000000001');
4. ⬆️ moveJobToFront(jobId)
Moves a job to the front of the flex queue (highest priority).
System.FlexQueue.moveJobToFront('707xx0000000001');
⚠️ All these methods return
true
if the move is successful orfalse
if no change was made.
📍 When to Use Flex Queue
When you need to submit more than 5 batch jobs but don’t want to wait.
When you want to prioritize urgent batch jobs.
When automating batch processes like data archiving, mass updates, etc.
📘 Example Use Case
You submit 8 jobs rapidly:
for (Integer i = 0; i < 8; i++) {
Database.executeBatch(new MyBatchClass(), 200);
}
The first 5 jobs go to Queued.
The remaining 3 go into Holding (Flex Queue).
You decide to prioritize job 8:
System.FlexQueue.moveJobToFront(job8Id);
Now job 8 will execute immediately after one of the 5 running jobs completes.
✅ What is System.isBatch()
?
System.isBatch()
returns:
true
→ if the current context of code execution is inside a batch class (i.e., withinstart
,execute
, orfinish
method of a batch).false
→ if called from any other context (e.g., synchronous Apex, trigger, controller).
🔍 Example:
public static void synchronusMethod() {
if(System.isBatch()) {
System.debug('Called from Batch Class');
} else {
System.debug('Called other than Batch class');
}
}
This is very useful when you want to alter behavior depending on whether the code is being executed in a batch context or not.
✅ How to Write a Test Class for Batch Apex
🔑 Key Guidelines:
Always use
Test.startTest()
andTest.stopTest()
around theDatabase.executeBatch()
call.Use a batch size such that the
execute()
method is called only once. This ensures that your test runs predictably and is easier to assert results.You cannot test multiple chunks of data in one unit test — only one chunk (i.e., one call to
execute()
) is guaranteed to run in a test context.
✅ Sample Test Class Template
Let’s say you're testing this batch class:
public class BatchClass implements Database.Batchable<sObject>{
public Database.QueryLocator start(Database.BatchableContext info) {
return Database.getQueryLocator('SELECT Id, Name FROM Account');
}
public void execute(Database.BatchableContext info, List<Account> accList) {
System.debug('Processing ' + accList.size());
}
public void finish(Database.BatchableContext info) {
System.debug('Batch finished');
}
}
🔧 Test Class:
@isTest
public class BatchClass_Test {
@isTest
static void testBatchExecution() {
// Test data setup
List<Account> accList = new List<Account>();
for (Integer i = 0; i < 5; i++) {
accList.add(new Account(Name = 'Test Acc ' + i));
}
insert accList;
// Test the batch execution
Test.startTest();
Database.executeBatch(new BatchClass(), 5); // Batch size = 5 so only one chunk
Test.stopTest();
// Optionally, add asserts or query changes made by execute method if applicable
System.debug('Batch executed in test context');
}
}
✅ Best Practices Summary:
Topic | Recommendation |
System.isBatch() | Use to detect if execution is inside a batch class |
Test.startTest() | Start capturing async operations |
Test.stopTest() | Forces all async jobs (including batch) to run synchronously |
Batch size for tests | Keep it small so execute() is called only once (usually ≤ number of test records) |
System.debug() vs Assert | Prefer System.assertEquals() for actual testing, debug() only for logging |
🔷 What is Database.Stateful
in Batch Apex?
By default, Batch Apex is stateless — meaning instance variables do not retain values between executions of the execute()
method.
Using Database.Stateful
allows you to persist instance variable values across all chunks of the batch job.
📌 When to Use:
You need to track totals, accumulate results, or collect data across batch chunks (e.g., email addresses, error logs, totals, etc.).
You plan to use the result in
finish()
to send a summary email, log output, etc.
🔧 How to Implement Database.Stateful
public class MyBatchClass implements Database.Batchable<sObject>, Database.Stateful {
// Instance variable that retains values across chunks
public Integer totalAccountsProcessed = 0;
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM Account');
}
public void execute(Database.BatchableContext bc, List<Account> accList) {
// This value is maintained across batch chunks
totalAccountsProcessed += accList.size();
}
public void finish(Database.BatchableContext bc) {
System.debug('Total Accounts Processed: ' + totalAccountsProcessed);
}
}
📬 Real-World Example – Send Email to Case Owners
You've already shared this — here's a summarized takeaway:
Why use Database.Stateful
here?
You're accumulating a list of email addresses across multiple batches. Without Stateful
, the list would be reset in every chunk, and you'd end up with an empty list in finish()
.
public class CaseOwnerEmailBatch implements Database.Batchable<sObject>, Database.Stateful {
List<String> emailList = new List<String>();
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Owner.Email FROM Case WHERE Status = \'New\'');
}
public void execute(Database.BatchableContext bc, List<Case> caseList) {
for (Case cs : caseList) {
emailList.add(cs.Owner.Email);
}
}
public void finish(Database.BatchableContext bc) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(emailList);
mail.setSubject('New Case Notification');
mail.setPlainTextBody('You have new case(s) assigned.');
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
⚠️ Limitations of Database.Stateful
Limitation | Description |
🔄 Slower Performance | The batch instance must be serialized and deserialized between chunks to preserve state. This adds overhead. |
💾 More Memory Use | Retaining state increases heap usage — careful when storing large lists or maps. |
🔄 Static Variables | Static variables are not preserved between chunks — only instance variables are. |
🧨 SaveResult Errors | If you store failed Database.SaveResult objects (with errors), it can cause "Internal Salesforce.com Error" when serialized. |
✅ Best Practices
Use
Stateful
only when truly needed — prefer stateless for performance.Keep instance variables lightweight (avoid heavy objects, deep maps, etc.).
Use
finish()
to do light final actions — avoid heavy logic there.
🔁 Implementing Both Database.Batchable
and Queueable
in a Single Class
Yes — you can implement both interfaces in the same class. Since both interfaces have an execute
method, you must overload the method signatures to distinguish them.
✅ Correct Implementation
public class BatchClass implements Database.Batchable<sObject>, Queueable {
// Required for Batchable
public Database.QueryLocator start(Database.BatchableContext info) {
String query = 'SELECT Id, Owner.Email FROM Case WHERE Status = \'New\'';
return Database.getQueryLocator(query);
}
// Required for Batchable
public void execute(Database.BatchableContext info, List<Case> caseList) {
System.debug('Inside batch execute method');
}
// Required for Batchable
public void finish(Database.BatchableContext info) {
System.debug('Inside batch finish method');
}
// Required for Queueable
public void execute(QueueableContext context) {
System.debug('Inside queueable execute method');
}
}
🚀 Behavior Based on Execution Method
Execution Code | Method(s) Invoked |
Database.executeBatch(new BatchClass()); | start , execute(Database.BatchableContext, List<sObject>) , finish |
System.enqueueJob(new BatchClass()); | execute(QueueableContext) |
⚠️ Things to Watch Out For
Avoid Logic Confusion: Make sure your logic is clearly separated for batch and queueable contexts. Don’t mix logic across both paths.
Governor Limits Are Different:
Batch Apex gets new limits per execute chunk.
Queueable Apex runs within a single context, with one set of limits.
Not a Common Pattern: It’s technically allowed, but rarely recommended. If you’re combining them, it should be for a very specific, justified use case (e.g., shared utility class or polymorphic interface usage).
You Can't Chain a Batch From a Queueable Directly: Chaining a batch inside
Queueable.execute()
is allowed, but not best practice unless you're doing controlled orchestration.
✅ When Would You Use This?
You might want to use a class that:
Acts as a Queueable job when you need light, async processing.
Acts as a Batch job when you're working with large data volumes.
To avoid code duplication, you implement both interfaces in one place.
🧠 Pro Tip
You can use flags or constructor parameters to help determine which logic to execute or customize behavior:
public class HybridJob implements Database.Batchable<sObject>, Queueable {
private Boolean isEmailMode = false;
public HybridJob(Boolean emailMode) {
this.isEmailMode = emailMode;
}
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM Contact');
}
public void execute(Database.BatchableContext bc, List<Contact> contacts) {
if (isEmailMode) {
// send emails
} else {
// do something else
}
}
public void finish(Database.BatchableContext bc) {
System.debug('Finished Batch');
}
public void execute(QueueableContext qc) {
System.debug('Running Queueable logic only');
}
}
✅ Passing Parameters to a Batch Class
Use constructors to pass custom parameters like recordId
into your batch class.
Example: Required Parameter
public class BatchClass implements Database.Batchable<sObject> {
Id accountId;
public BatchClass(Id accountId) {
this.accountId = accountId;
}
public Database.QueryLocator start(Database.BatchableContext info) {
return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE Id = :accountId');
}
public void execute(Database.BatchableContext info, List<Account> accountList) {
System.debug('Accounts size: ' + accountList.size());
}
public void finish(Database.BatchableContext info) {}
}
Run:
Database.executeBatch(new BatchClass('0015g00000uyI6OAAU'));
✅ Optional Parameters via Constructor Overloading
Handle the case where recordId
might be optional.
public class BatchClass implements Database.Batchable<sObject> {
Id accountId;
String query = 'SELECT Id, Name FROM Account';
public BatchClass() {}
public BatchClass(Id accountId) {
this.accountId = accountId;
query += ' WHERE Id = :accountId';
}
public Database.QueryLocator start(Database.BatchableContext info) {
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext info, List<Account> accountList) {
System.debug('Accounts size: ' + accountList.size());
}
public void finish(Database.BatchableContext info) {}
}
Run:
Database.executeBatch(new BatchClass()); // All records
Database.executeBatch(new BatchClass('0015g00000uyI6OAAU')); // Specific record
🌐 Making Callouts from Batch Apex
To make HTTP callouts, implement the Database.AllowsCallouts
interface.
Batch Class with Callout
public class BatchClass implements Database.Batchable<sObject>, Database.AllowsCallouts {
public Database.QueryLocator start(Database.BatchableContext info) {
return Database.getQueryLocator('SELECT Id, Name FROM Account');
}
public void execute(Database.BatchableContext info, List<Account> accountList) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://gorest.co.in/public/v2/posts');
req.setMethod('GET');
HttpResponse res = new Http().send(req);
System.debug('Status Code: ' + res.getStatusCode());
System.debug('Body: ' + res.getBody());
}
public void finish(Database.BatchableContext info) {}
}
⚠️ Make sure:
The endpoint is added in Remote Site Settings
You use the
Database.AllowsCallouts
interfaceYou're aware of callout limits (100 per transaction)
🧪 Test Class for Callout-Based Batch Apex
Use HttpCalloutMock
to simulate responses.
Mock Class
@isTest
global class CalloutMock implements HttpCalloutMock {
global HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('Test JSON Body');
return res;
}
}
Test Class
@isTest
public class BatchClass_Test {
@testSetup
static void setupTestData() {
List<Account> accList = new List<Account>();
for (Integer i = 1; i <= 200; i++) {
accList.add(new Account(Name = 'Test Account ' + i));
}
insert accList;
}
@isTest
static void testBatchWithCallout() {
Test.setMock(HttpCalloutMock.class, new CalloutMock());
Test.startTest();
Database.executeBatch(new BatchClass(), 200); // limit batch size so that only one callout is tested
Test.stopTest();
}
}
✅ Best Practices Recap
Always wrap
executeBatch
calls inTest.startTest()
/Test.stopTest()
in testsUse constructor overloading for optional parameters
Implement
Database.AllowsCallouts
to enable HTTP callsUse mocks to simulate external callouts and avoid hitting real APIs in tests
Set appropriate batch sizes in test classes to ensure predictable test behavior
✅ Batch Apex Best Practices
⚠️ Trigger Usage
Avoid invoking batch jobs directly from triggers.
Triggers can fire in bulk—potentially exceeding the 5 concurrent jobs or flex queue limit.
Consider queueable or future alternatives and add guardrails like
Limits.getQueueableJobs()
.
🧪 Testing Batch Apex
Wrap batch calls in
Test.startTest()
andTest.stopTest()
to ensure execution completes during test.Use small scope sizes in tests to avoid governor limits.
You can test only a single execute() run, so limit scope (e.g.,
Database.executeBatch(batchInstance, 200);
).
🔄 Stateful vs Stateless
Use
Database.Stateful
to retain instance variable values across transactions.By default, batches are stateless — member variables reset between transactions.
Use stateful batches cautiously: they consume more memory and may slow performance.
⚙️ Asynchronous Execution Considerations
Database.executeBatch()
only queues the job.Actual execution depends on system resources and may be delayed.
Batch jobs survive Salesforce downtime — they'll rerun from the beginning if interrupted.
📧 Notifications & Logging
Notifications sent to the user who submitted the batch or, in managed packages, to the Apex Exception Notification Recipient.
Use the
AsyncApexJob
object to monitor job status, progress, errors:SELECT Id, Status, JobItemsProcessed, NumberOfErrors, TotalJobItems FROM AsyncApexJob WHERE JobType = 'BatchApex' AND CreatedDate = TODAY
Exclude
JobType = 'BatchApexWorker'
in large orgs, as one worker record is created per 10,000 jobs.
⚡ Performance Tuning
Reduce Web service callout time in batches.
Optimize SOQL queries for selectivity and index usage.
Minimize batch size and batch count where possible to avoid overwhelming the async processing queue.
Keep jobs short-running to avoid delaying other async tasks.
🚫 Batch Apex Limitations
Limitation | Value |
Concurrent jobs (active or queued) | 5 |
Flex Queue "Holding" jobs | 100 |
Daily batch executions per org | 250,000 |
Start method concurrency | Only 1 start() runs at a time |
Callouts per batch job (across start, execute, finish) | 10 |
Future methods in batch classes | ❌ Not allowed |
Future methods from batch classes | ❌ Not allowed |
🧠 Pro Tips
Chaining batch jobs: You can queue the next batch from the
finish()
method of the current one.Bulk-safe code: Always code for bulk operations (e.g., API loads, mass updates).
Use
Database.executeBatch(new MyBatchClass(), scopeSize)
wherescopeSize
is tuned to optimize for:Governor limits (e.g., DMLs, SOQLs)
Available heap size
Callout counts (if any)
Subscribe to my newsletter
Read articles from Black BUC directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
