Batch Apex: Complete Guide with Examples | SalesforceTutorial

Written by Prasanth Kumar Published on Updated on

Batch apex allows you to process large datasets by dividing operations into manageable chunks. Each chunk processes separately, avoiding governor limits that restrict single transactions to 10,000 records. This guide covers implementation patterns, best practices, and common pitfalls for Salesforce developers.

What is Batch Apex?

Batch apex is an asynchronous processing framework that handles large-scale data operations by breaking them into smaller batches. When you need to update thousands of records, batch apex divides the work into chunks of up to 2,000 records per batch (default 200), processing each batch separately within its own transaction context.

For example, updating 50,000 Account records would normally hit governor limits in a single transaction. Batch apex automatically divides these records into 250 batches of 200 records each, processing them sequentially without limit violations.

Database.Batchable Interface Methods

To implement batch apex, your class must implement the Database.Batchable<sObject> interface, which defines three required methods:

1. Start Method

The start method executes first and defines which records to process. It returns either a Database.QueryLocator (for SOQL queries) or an Iterable<sObject> (for custom collections).

global Database.QueryLocator start(Database.BatchableContext BC) {
    String query = 'SELECT Id, Name FROM Account WHERE CreatedDate = LAST_N_DAYS:30';
    return Database.getQueryLocator(query);
}

2. Execute Method

The execute method processes each batch of records. The scope parameter contains up to 2,000 records (configurable when calling Database.executeBatch).

global void execute(Database.BatchableContext BC, List<Account> scope) {
    List<Account> accountsToUpdate = new List<Account>();
    
    for(Account acc : scope) {
        acc.Name = acc.Name + ' - Updated';
        accountsToUpdate.add(acc);
    }
    
    if(!accountsToUpdate.isEmpty()) {
        update accountsToUpdate;
    }
}

3. Finish Method

The finish method executes after all batches complete. Use it for cleanup operations, sending notifications, or chaining additional batch jobs.

global void finish(Database.BatchableContext BC) {
    // Send completion email
    AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
                       TotalJobItems, CreatedBy.Email FROM AsyncApexJob WHERE Id = :BC.getJobId()];
    
    // Email logic here
}

Complete Batch Apex Example

Here’s a production-ready batch class that updates Account names with error handling and logging:

global class BatchAccountUpdate implements Database.Batchable<sObject>, Database.Stateful {
    
    global Integer recordsProcessed = 0;
    global Integer errorCount = 0;
    
    global Database.QueryLocator start(Database.BatchableContext BC) {
        // Query only active accounts created in last 30 days
        String query = 'SELECT Id, Name FROM Account WHERE IsDeleted = false AND CreatedDate = LAST_N_DAYS:30';
        return Database.getQueryLocator(query);
    }
    
    global void execute(Database.BatchableContext BC, List<Account> scope) {
        List<Account> accountsToUpdate = new List<Account>();
        
        for(Account acc : scope) {
            if(String.isNotBlank(acc.Name)) {
                acc.Name = acc.Name + ' - Batch Updated';
                accountsToUpdate.add(acc);
            }
        }
        
        if(!accountsToUpdate.isEmpty()) {
            try {
                Database.SaveResult[] results = Database.update(accountsToUpdate, false);
                
                for(Database.SaveResult result : results) {
                    if(result.isSuccess()) {
                        recordsProcessed++;
                    } else {
                        errorCount++;
                        System.debug('Error updating account: ' + result.getErrors());
                    }
                }
            } catch(Exception e) {
                System.debug('Batch execution error: ' + e.getMessage());
                errorCount += accountsToUpdate.size();
            }
        }
    }
    
    global void finish(Database.BatchableContext BC) {
        System.debug('Batch completed. Records processed: ' + recordsProcessed + ', Errors: ' + errorCount);
        
        // Query job details
        AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
                           TotalJobItems FROM AsyncApexJob WHERE Id = :BC.getJobId()];
        
        // Send notification email or create log record
    }
}

How to Execute Batch Apex

Database.executeBatch Method

Use Database.executeBatch to start your apex batch job. The method accepts your batch class instance and an optional batch size parameter.

// Execute with default batch size (200)
BatchAccountUpdate batchJob = new BatchAccountUpdate();
Id jobId = Database.executeBatch(batchJob);

// Execute with custom batch size (500 records per batch)
Id jobId2 = Database.executeBatch(batchJob, 500);

Run Batch Class from Developer Console

To run batch class from developer console:

  1. Open Developer Console
  2. Go to Debug → Open Execute Anonymous Window
  3. Enter your batch execution code:
BatchAccountUpdate batch = new BatchAccountUpdate();
Database.executeBatch(batch, 200);
  • Click Execute
  • Monitor progress in Setup → Apex Jobs
  • Salesforce Batch Processing Best Practices

    Governor Limits in Batch Apex

    • SOQL Queries: 100 per batch execution (not per record)
    • DML Statements: 150 per batch execution
    • Records per batch: Maximum 2,000, default 200
    • Heap Size: 12MB per batch execution
    • CPU Time: 60 seconds per batch execution

    Performance Optimization

    • Use selective SOQL queries with indexed fields
    • Implement Database.Stateful for cross-batch variable persistence
    • Consider using Database.AllowsCallouts for external integrations
    • Batch size affects performance: smaller batches = more overhead, larger batches = higher memory usage

    Error Handling Patterns

    // Use Database.update with allOrNone = false for partial success
    Database.SaveResult[] results = Database.update(recordsToUpdate, false);
    
    for(Integer i = 0; i < results.size(); i++) {
        if(!results[i].isSuccess()) {
            System.debug('Failed to update record: ' + recordsToUpdate[i].Id);
            // Log error details
        }
    }

    Test Class for Batch Apex

    Test classes for batch apex require specific patterns to achieve proper code coverage:

    @isTest
    public class BatchAccountUpdateTest {
        
        @testSetup
        static void setupTestData() {
            List<Account> testAccounts = new List<Account>();
            
            for(Integer i = 0; i < 200; i++) {
                testAccounts.add(new Account(
                    Name = 'Test Account ' + i,
                    Industry = 'Technology'
                ));
            }
            
            insert testAccounts;
        }
        
        @isTest
        static void testBatchExecution() {
            Test.startTest();
            
            BatchAccountUpdate batchJob = new BatchAccountUpdate();
            Id jobId = Database.executeBatch(batchJob, 50);
            
            Test.stopTest();
            
            // Verify results
            List<Account> updatedAccounts = [SELECT Id, Name FROM Account WHERE Name LIKE '%Batch Updated%'];
            System.assertEquals(200, updatedAccounts.size(), 'All accounts should be updated');
            
            // Verify job completion
            AsyncApexJob job = [SELECT Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];
            System.assertEquals('Completed', job.Status, 'Job should complete successfully');
            System.assertEquals(0, job.NumberOfErrors, 'No errors should occur');
        }
        
        @isTest
        static void testBatchWithErrors() {
            // Create account with null name to trigger validation error
            Account errorAccount = new Account(Name = null);
            insert errorAccount;
            
            Test.startTest();
            
            BatchAccountUpdate batchJob = new BatchAccountUpdate();
            Database.executeBatch(batchJob);
            
            Test.stopTest();
            
            // Verify error handling
            System.assert(batchJob.errorCount > 0, 'Error count should be tracked');
        }
    }

    Test Class for Schedulable Batch Class

    When your batch class implements both Database.Batchable and Schedulable interfaces:

    global class SchedulableBatchAccount implements Database.Batchable<sObject>, Schedulable {
        
        // Schedulable interface method
        global void execute(SchedulableContext SC) {
            BatchAccountUpdate batch = new BatchAccountUpdate();
            Database.executeBatch(batch, 200);
        }
        
        // Batchable interface methods...
    }

    Test class pattern:

    @isTest
    public class SchedulableBatchAccountTest {
        
        @isTest
        static void testSchedulableBatch() {
            Test.startTest();
            
            SchedulableBatchAccount schedulable = new SchedulableBatchAccount();
            String cronExp = '0 0 2 * * ?'; // Daily at 2 AM
            String jobId = System.schedule('Test Schedulable Batch', cronExp, schedulable);
            
            Test.stopTest();
            
            // Verify scheduled job was created
            CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered FROM CronTrigger WHERE Id = :jobId];
            System.assertEquals(cronExp, ct.CronExpression);
        }
    }

    Monitoring Batch Jobs

    Monitor batch job progress through Setup → Apex Jobs or query AsyncApexJob records:

    List<AsyncApexJob> jobs = [SELECT Id, Status, JobType, MethodName, 
                              NumberOfErrors, JobItemsProcessed, TotalJobItems,
                              CreatedDate, CompletedDate
                              FROM AsyncApexJob 
                              WHERE JobType = 'BatchApex' 
                              AND CreatedDate = TODAY
                              ORDER BY CreatedDate DESC];

    Common Batch Apex Pitfalls

    • Governor Limit Violations: Avoid SOQL queries inside loops within execute method
    • Memory Issues: Don’t store large collections in instance variables without Database.Stateful
    • Callout Limitations: Implement Database.AllowsCallouts for HTTP requests
    • Transaction Boundaries: Each batch executes in separate transaction – no rollback across batches
    • Concurrent Execution: Maximum 5 batch jobs can run simultaneously

    Frequently Asked Questions

    How do I run batch class from developer console?

    Open Developer Console, go to Debug → Open Execute Anonymous Window, enter your batch execution code like Database.executeBatch(new YourBatchClass(), 200);, then click Execute. Monitor progress in Setup → Apex Jobs.

    What is the maximum batch size for batch apex?

    The maximum batch size is 2,000 records per batch. The default is 200 records. You can specify batch size as the second parameter in Database.executeBatch(batchInstance, batchSize).

    How do I write test class for batch class?

    Create test data in @testSetup, call Database.executeBatch() between Test.startTest() and Test.stopTest(), then verify results. Use AsyncApexJob queries to check job status and error counts for complete coverage.

    Can batch apex make callouts to external systems?

    Yes, but your batch class must implement Database.AllowsCallouts interface. Each batch execution has a limit of 100 callouts. Consider using @future methods or Queueable Apex for better callout handling.

    How many batch jobs can run simultaneously?

    Salesforce allows maximum 5 batch jobs to run concurrently per org. Additional jobs queue until a slot becomes available. Use AsyncApexJob to monitor job status and queue position.

    What happens if a batch fails in the middle?

    Each batch executes in its own transaction. If one batch fails, previously completed batches remain committed. Failed batches don’t automatically retry – implement custom retry logic in your finish() method if needed.