Study guide
9.3 Batch Apex
Salesforce Certified Platform Developer I Exam
Track your progress as you read
Finished sections get a check mark.
1 Introduction & Exam Overview
1.1 Current Exam Facts 1.2 Blueprint Weights & Study Strategy
2 Platform Fundamentals
2.1 Multi-Tenant Architecture & Shared Resources 2.2 MVC Mapping on the Lightning Platform 2.3 Declarative vs Programmatic Decisions 2.4 Agentforce for Developers: Use Cases & Limitations
3 Data Modeling & Schema
3.1 Objects, Fields & Schema Design 3.2 Relationship Types & Implications 3.3 Schema Design Impact on Apex 3.4 Data Import & Export for Development
4 Platform Data Features
4.1 Formula Fields & Roll-Up Summaries 4.2 Core CRM Objects & Standard Schema 4.3 AppExchange Extensions & Platform Boundaries
5 Declarative Process Automation
5.1 Record-Triggered Flows & Declarative Automation 5.2 Approval Processes & Business Rules 5.3 When to Use Apex vs Declarative Tools
6 Apex Language Basics
6.1 Variables, Constants & Apex Data Types 6.2 Lists, Sets & Maps 6.3 Control Flow & Exception Handling 6.4 Classes, Interfaces & OOP Patterns
7 SOQL, SOSL & DML
7.1 SOQL Query Fundamentals 7.2 SOSL Search Fundamentals 7.3 DML Operations & Bulkification 7.4 Governor Limits & Transaction Boundaries
8 Apex Triggers & Order of Execution
8.1 Trigger Structure & Context Variables 8.2 Bulk Trigger Patterns & Best Practices 8.3 Save Order of Execution 8.4 Recursion, Cascading & Transaction Control
9 Asynchronous Apex
9.1 @future Methods 9.2 Queueable Apex 9.3 Batch Apex 9.4 Schedulable Apex & Job Orchestration
10 Security, Events & Schema Access
10.1 Sharing, FLS, CRUD & Injection Prevention 10.2 Platform Events Publish & Subscribe 10.3 Programmatic Schema Access (Describe)
11 Visualforce
11.1 Visualforce Pages & Markup 11.2 Standard, Custom Controllers & Extensions 11.3 Visualforce in Lightning Experience
12 Lightning Components
12.1 Lightning Component Framework Benefits 12.2 Aura Component Structure & Resources 12.3 Lightning Web Components Fundamentals 12.4 LWC Data Access: Wire, Imperative & Events
13 UI Composition & Apex Integration Patterns
13.1 Composing UI in Lightning Apps 13.2 UI Security, Performance & Accessibility Patterns 13.3 Apex Actions for Flow, LWC, and Agentforce
14 Apex Testing
14.1 Testing Framework & Coverage Requirements 14.2 Writing Unit Tests for Triggers & Classes 14.3 Test Data Strategies & Isolation 14.4 Executing Tests & Execute Anonymous Differences
15 Debugging & Developer Tools
15.1 Debug Logs & Monitoring 15.2 Developer Console, CLI, VS Code & Workbench 15.3 Sandboxes, Scratch Orgs & Environment Strategy
16 Deployment & Release
16.1 Change Sets & Metadata Deployment 16.2 Unlocked Packages, Metadata API & Salesforce DX 16.3 Release Readiness & Credential Maintenance
9.3 Batch Apex
Key Takeaways Explain This Topic AI
Database.Batchable defines start, execute, and finish; start returns a QueryLocator or Iterable that defines the full workset
QueryLocator can address very large volumes (far beyond a single synchronous query list); Iterable is for custom iteration when a single SOQL locator is not enough
execute runs repeatedly for each scope (batch size); governor limits apply per execute invocation, not once for the whole job
Database.Stateful preserves instance variable state across execute calls; without it, non-static instance fields reset each execute
Use Batch for large data volumes; use Queueable for lighter deferred work—chain batches from finish and monitor via AsyncApexJob
Last updated: August 2026
9.3 Batch Apex
Quick Answer: Batch Apex implements Database.Batchable with start , execute , and finish . start returns a QueryLocator (preferred for large SOQL sets) or Iterable ; the platform calls execute once per scope chunk with fresh governor limits; finish runs cleanup or chaining. Use batch for large data volumes ; keep Queueable for smaller post-commit jobs.
When a nightly job must touch hundreds of thousands of records, synchronous Apex and even single Queueable executes hit heap, CPU, or query row walls. Batch Apex is the platform’s built-in chunking engine—and a high-value Platform Developer I topic.
- Database.Batchable Anatomy public class AccountIndustryBatch
- implements Database.Batchable<SObject>, Database.Stateful {
public Integer recordsProcessed = 0;
- public Database.QueryLocator start(Database.BatchableContext bc) {
- return Database.getQueryLocator([
- SELECT Id, Industry, Description
- FROM Account
- WHERE Industry = null
- ]);
- }
- public void execute(Database.BatchableContext bc, List<SObject> scope) {
- List<Account> updates = new List<Account>();
- for (SObject sob : scope) {
- Account a = (Account)sob;
- a.Description = 'Industry review required';
- updates.add(a);
- recordsProcessed++;
- }
- update updates;
- }
- public void finish(Database.BatchableContext bc) {
- System.debug('Processed: ' + recordsProcessed);
- // optional: email admin, chain next batch, enqueue Queueable
- }
- }
- Launch the job: Id batchJobId = Database.executeBatch(new AccountIndustryBatch(), 200);
The optional second argument is the scope size (records per execute ). Default is often 200 when omitted (confirm behavior in your API version docs; exams treat 200 as the familiar default). Maximum scope is capped by the platform (commonly discussed as up to 2000 ). Smaller scopes reduce per-execute heap/CPU risk; larger scopes reduce the number of execute rounds but raise per-chunk governor pressure.
- start: QueryLocator vs Iterable
- Database.QueryLocator public Database.QueryLocator start(Database.BatchableContext bc) {
- return Database.getQueryLocator(
- 'SELECT Id, Name FROM Contact WHERE Email = null'
- );
- }
Why QueryLocator wins for scale:
Designed for very large result sets (on the order of tens of millions of rows in batch context—far beyond normal synchronous query list patterns).
The platform manages cursor-like retrieval into scopes.
Ideal when a single SOQL statement defines the entire population.
- Iterable public Iterable<Id> start(Database.BatchableContext bc) {
- // Custom list of Ids from complex pre-logic, callouts, or multiple sources
- return someIdList;
- }
Use Iterable when:
Work items are not a single straightforward SOQL locator (custom iteration, mixed sources).
You need to batch over non-sObject types (for example, Iterable<String> of external keys) with a matching Batchable<String> style generic.
Iterable batches still chunk into scopes, but they do not replace QueryLocator’s massive SOQL volume advantages when pure SOQL defines the set. Exam: if the stem says “millions of Account records matching a filter,” answer QueryLocator .
execute: Scope Size and Per-Chunk Governors
Each execute invocation receives a List of at most scope records and runs as its own transaction-like unit with governor limits reset for that execute (a critical exam idea).
Implications:
- Topic What to remember
- SOQL/DML in execute Limits apply per execute , so bulk patterns still required inside the chunk
Failure isolation One scope can fail without necessarily undoing other completed scopes (design for partial progress)
Trigger re-entry Updates in execute fire triggers—bulk-safe triggers remain mandatory
Heap Scope size is your primary dial when execute hits heap public void execute(Database.BatchableContext bc, List<Contact> scope) {
// Still bulkify inside the scope!
- Set<Id> accountIds = new Set<Id>();
- for (Contact c : scope) {
- if (c.AccountId != null) accountIds.add(c.AccountId);
- }
- Map<Id, Account> accounts = new Map<Id, Account>([
- SELECT Id, Name FROM Account WHERE Id IN :accountIds
- ]);
// ... use map, then one DML ...
}
Anti-pattern: SOQL inside a per-record loop within execute. You only have 200 (or N) records, but you can still blow SOQL limits if you query per row.
Database.Stateful
By default, instance variables on the batch class do not retain changes across execute calls the way beginners expect—each execute may work with a fresh deserialized state unless you implement Database.Stateful . public class CountingBatch implements Database.Batchable<SObject>, Database.Stateful {
public Integer total = 0;
- public Database.QueryLocator start(Database.BatchableContext bc) {
- return Database.getQueryLocator('SELECT Id FROM Lead WHERE IsConverted = false');
- }
- public void execute(Database.BatchableContext bc, List<SObject> scope) {
- total += scope.size(); // preserved across executes because Stateful
- }
- public void finish(Database.BatchableContext bc) {
- System.debug('Total leads in job: ' + total);
- }
- }
Use Stateful for: running totals, collected error Id lists (within reason), flags, small aggregate metrics for finish emails.
Avoid Stateful for: huge collections that grow every execute—state is serialized between chunks and can hit heap/async limits. Prefer writing progress to custom objects if you must track massive intermediate data.
Static variables are not a substitute for Stateful across batch executes in the way candidates hope; rely on Stateful instance fields or durable storage.
Callouts in Batch
Implement Database.AllowsCallouts : public class SyncBatch implements Database.Batchable<SObject>, Database.AllowsCallouts {
- // start / execute with Http callouts per scope / finish
- }
Callout limits still apply per execute . If each record needs a callout, choose a small scope size (sometimes 1–10) so you stay under callout counts per transaction. That is a classic design tradeoff on scenario questions.
Chaining Batches and finish()
finish runs once after all executes complete (or when the job ends). Common uses:
- Send a summary email or platform event
- Launch a second batch for the next object domain
- System.enqueueJob for a lightweight post-step
- Update a custom “job run” record with status public void finish(Database.BatchableContext bc) {
- Database.executeBatch(new RelatedOpportunityBatch(), 200);
- }
Chaining from finish keeps large multi-object migrations ordered: Accounts → Contacts → Opportunities, each as its own batch job.
- Monitoring with AsyncApexJob AsyncApexJob job = [
- SELECT Id, Status, JobType, NumberOfErrors,
- JobItemsProcessed, TotalJobItems, CreatedDate, CompletedDate
- FROM AsyncApexJob
- WHERE Id = :batchJobId
- ];
Admins also monitor under Setup → Apex Jobs . Status values include Queued, Holding, Preparing, Processing, Completed, Failed, Aborted. JobItemsProcessed / TotalJobItems reflect batch chunks, useful for progress UIs and support.
Database.BatchableContext provides getJobId() inside start/execute/finish for the same correlation.
Batch vs Queueable for Large Data Volumes
- Situation Prefer
- Hundreds of thousands / millions of rows, SOQL-defined set Batch + QueryLocator
- Post-trigger callout for ≤ hundreds of records Queueable
- Need flexible chaining of small steps with rich state Queueable
- Need scheduled nightly sweep of an object Schedulable → Batch
- Must tune callouts per limited transaction Batch with small scope
- Simple deferred Id processing Queueable or @future
Rule of thumb taught on the exam: Batch is for volume and chunking ; Queueable is for flexible async units of work . Using Queueable in a loop to fake batching is an anti-pattern when Batchable exists.
- Testing Batch Apex @IsTest
- static void batchUpdatesAccounts() {
- List<Account> data = new List<Account>();
- for (Integer i = 0; i < 50; i++) {
- data.add(new Account(Name = 'Batch ' + i));
- }
- insert data;
- Test.startTest();
- Database.executeBatch(new AccountIndustryBatch(), 50);
- Test.stopTest();
- // assert post-batch field values
- }
Test.stopTest() forces the batch to run to completion in tests (start, all executes, finish). Keep test data volumes modest but non-trivial; you do not need millions of rows in unit tests.
Exam Checklist
Name the three methods: start / execute / finish .
Choose QueryLocator for large SOQL sets; Iterable for custom sets.
Know scope size trades throughput vs per-execute limits.
Stateful for cross-execute instance totals.
AllowsCallouts + small scope when calling out per record.
Governors are per execute .
Monitor with AsyncApexJob ; chain from finish .
Prefer batch over queueable when the problem is scale , not just “run later.”
Batch Apex is how Salesforce apps process org-scale data safely—chunk by chunk, limit by limit, job by job.