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.