Batch Apex

Black BUCBlack BUC
15 min read

⚙️ 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:

MethodDescription
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

FeatureLimit
SOQL Rows RetrievedUp to 50 million (Database.QueryLocator)
Default Batch Size200 records per chunk
Max Simultaneous Batch Jobs5 active batch jobs at a time
Max Records in Iterable50 million
Max Batch Apex Invocations/Day250,000 or number of user licenses * 200, whichever is greater
Heap Size per Transaction12 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

FeatureBatch ApexQueueable Apex
Best forMillions of recordsThousands of records
Retry capabilityYes (automatic retry possible)No built-in retry
ChainingLimited via finish() methodEasier using System.enqueueJob()
Scope SizeAdjustable (default: 200)Not chunked
Governor Limits ResetPer batch executionOnly once per job
ComplexityHigher setupSimpler 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 an Iterable<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() returns null if used in start() or finish().


🔄 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

StatusDescription
HoldingJob submitted but waiting in flex queue.
QueuedJob moved from holding to ready-to-execute state.
PreparingStart method is running; data is being loaded.
ProcessingJob is currently executing (inside execute method).
CompletedJob finished successfully (some failures may be allowed).
FailedJob failed due to system error or unhandled exception.
AbortedJob 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 or false 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., within start, execute, or finish 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() and Test.stopTest() around the Database.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:

TopicRecommendation
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 testsKeep it small so execute() is called only once (usually ≤ number of test records)
System.debug() vs AssertPrefer 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

LimitationDescription
🔄 Slower PerformanceThe batch instance must be serialized and deserialized between chunks to preserve state. This adds overhead.
💾 More Memory UseRetaining state increases heap usage — careful when storing large lists or maps.
🔄 Static VariablesStatic variables are not preserved between chunks — only instance variables are.
🧨 SaveResult ErrorsIf 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 CodeMethod(s) Invoked
Database.executeBatch(new BatchClass());start, execute(Database.BatchableContext, List<sObject>), finish
System.enqueueJob(new BatchClass());execute(QueueableContext)

⚠️ Things to Watch Out For

  1. Avoid Logic Confusion: Make sure your logic is clearly separated for batch and queueable contexts. Don’t mix logic across both paths.

  2. Governor Limits Are Different:

    • Batch Apex gets new limits per execute chunk.

    • Queueable Apex runs within a single context, with one set of limits.

  3. 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).

  4. 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 interface

  • You'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 in Test.startTest() / Test.stopTest() in tests

  • Use constructor overloading for optional parameters

  • Implement Database.AllowsCallouts to enable HTTP calls

  • Use 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() and Test.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

LimitationValue
Concurrent jobs (active or queued)5
Flex Queue "Holding" jobs100
Daily batch executions per org250,000
Start method concurrencyOnly 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) where scopeSize is tuned to optimize for:

    • Governor limits (e.g., DMLs, SOQLs)

    • Available heap size

    • Callout counts (if any)


0
Subscribe to my newsletter

Read articles from Black BUC directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Black BUC
Black BUC