Study guide
8.2 Bulk Trigger Patterns & Best Practices
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
8.2 Bulk Trigger Patterns & Best Practices
Key Takeaways Explain This Topic AI
Always assume Trigger.new can contain up to 200 records—collect Ids, query once, and use Map lookups instead of SOQL or DML inside per-record loops
Keep triggers thin: route context to a handler class; put domain logic and data access in service classes
Guard against recursion with static flags or processed-Id sets when after-context DML would re-fire the same trigger
Prefer Flow (or other declarative tools) when the requirement is simple field updates or maintainable admin automation without complex bulk algorithms
Framework pattern trigger → handler → service scales testing, bypasses, and multi-event routing without duplicating SOQL
Last updated: August 2026
8.2 Bulk Trigger Patterns & Best Practices
Quick Answer: Write every trigger as if 200 records arrived at once. Collect related Ids into a Set, run one SOQL , build a Map for lookups, apply rules in memory, then perform one DML on a list or map of changes. Put that logic in handler/service classes , not a fat trigger body. Use static recursion guards when your DML would re-enter the same trigger, and prefer Flow when declarative automation fully meets the requirement.
Platform Developer I hammers bulkification. A snippet that works for one Account in the UI and fails in Data Loader is the classic wrong answer—even if syntax compiles.
- The Core Bulk Pattern: Collect → Query → Map → Act
- Anti-pattern (fails governors): // BAD: SOQL and DML inside a per-record loop
- for (Contact c : Trigger.new) {
- Account a = [SELECT Id, Industry FROM Account WHERE Id = :c.AccountId];
- if (a.Industry == 'Agriculture') {
- c.Description = 'Agro contact';
- update c; // also wrong in before; disastrous in after
- }
- }
With 200 contacts this can burn hundreds of SOQL and DML operations and hit limits immediately.
- Bulk-safe pattern: // GOOD: one query, map lookup, field edits in before context
- Set<Id> accountIds = new Set<Id>();
- for (Contact c : Trigger.new) {
- if (c.AccountId != null) {
- accountIds.add(c.AccountId);
- }
- }
- Map<Id, Account> accountsById = new Map<Id, Account>();
- if (!accountIds.isEmpty()) {
- accountsById = new Map<Id, Account>([
- SELECT Id, Industry
- FROM Account
- WHERE Id IN :accountIds
- ]);
- }
- for (Contact c : Trigger.new) {
- Account parent = accountsById.get(c.AccountId);
- if (parent != null && parent.Industry == 'Agriculture') {
- c.Description = 'Agro contact'; // before update/insert: no DML needed
- }
- }
- When related records must be written in after context: List<Task> toInsert = new List<Task>();
- for (Opportunity o : Trigger.new) {
- if (o.StageName == 'Closed Won') {
- toInsert.add(new Task(
- WhatId = o.Id,
- Subject = 'Post-win review',
- OwnerId = o.OwnerId,
- Status = 'Not Started'
- ));
- }
- }
- if (!toInsert.isEmpty()) {
- insert toInsert; // single DML outside the loop
- }
- Rules of thumb
- Never in a per-record loop Do instead
- SOQL Collect Ids → one WHERE Id IN :ids
- DML ( insert / update / delete ) Build List/Map → one DML after loops
- SOQL aggregate per row One aggregate query grouped by key, or roll up in maps
- Callouts Collect work → async ( @future , Queueable) after bulk prep
- Handler and Service Layering (Framework Pattern)
Fat trigger (logic, queries, and DML all in the .trigger file) is hard to test, reuse, and bypass. Preferred layering: trigger AccountTrigger on Account (...) {
- AccountTriggerHandler.dispatch();
- }
- │
- ▼
- AccountTriggerHandler → routes by operationType / isBefore flags
- │
- ▼
- AccountService / AccountRelatedService → pure domain methods, SOQL/DML
- Example skeleton: public with sharing class AccountTriggerHandler {
- public static void dispatch() {
- switch on Trigger.operationType {
- when BEFORE_INSERT, BEFORE_UPDATE {
- AccountService.applyDefaults((List<Account>)Trigger.new);
- }
- when AFTER_UPDATE {
- AccountService.syncRelatedOnIndustryChange(
- (List<Account>)Trigger.new,
- (Map<Id, Account>)Trigger.oldMap
- );
- }
- }
- }
- }
- public with sharing class AccountService {
- public static void applyDefaults(List<Account> rows) {
- for (Account a : rows) {
- if (a.Type == null) {
- a.Type = 'Prospect';
- }
- }
- }
- public static void syncRelatedOnIndustryChange(
- List<Account> news,
- Map<Id, Account> oldMap
- ) {
- Set<Id> changedIds = new Set<Id>();
- for (Account a : news) {
- Account prior = oldMap.get(a.Id);
- if (prior != null && a.Industry != prior.Industry) {
- changedIds.add(a.Id);
- }
- }
- if (changedIds.isEmpty()) {
- return;
- }
// query children once, build updates, single DML...
- }
- }
- Benefits for the exam and real orgs
- Unit tests call service methods with constructed lists without performing full UI saves for every path
- Recursion and bypass flags live in one place
- Multiple events share collection helpers without copy-paste SOQL
- Trigger file stays a few lines—easy code review
Some orgs use a lightweight TriggerHandler base class with beforeInsert() , afterUpdate() , and a static bypass set. The exam does not require a specific open-source framework name; it does expect the idea : thin trigger, bulk handler, no SOQL/DML in loops.
Recursion Guards (Introduction)
After-context DML on the same object re-fires triggers and can recurse until CPU or DML limits fail. Simple static guard: public with sharing class OpportunityTriggerHandler {
private static Boolean isRunning = false;
- public static void afterUpdate(List<Opportunity> news, Map<Id, Opportunity> oldMap) {
- if (isRunning) {
- return;
- }
- isRunning = true;
- try {
- // related updates that might bounce back onto Opportunity
- OpportunityService.rollupToParents(news, oldMap);
- } finally {
- isRunning = false;
- }
- }
- }
Boolean guards are blunt: they block all re-entry for the rest of that path. Finer control uses a static Set<Id> of already-processed Ids so other records in a later chunk can still run. Full recursion, cascading, and transaction control are covered in 8.4; here, know that bulk patterns plus static guards are the standard defense.
Selective Logic: Only Work When Fields Change
Bulk CPU burns when every update re-processes unchanged rows. Compare old vs new: for (Account a : Trigger.new) {
- Account prior = Trigger.oldMap.get(a.Id);
- if (prior.OwnerId == a.OwnerId) {
- continue; // ownership did not change—skip expensive path
- }
- // collect for owner-change side effects
- }
Combine change detection with Sets/Maps so side-effect queries only run for true deltas.
- Error Handling in Bulk
- Prefer before addError for business validation so bad rows fail cleanly
If using Database.update(records, false) , inspect every Database.SaveResult —partial success is not silent success
Do not empty-catch DmlException around bulk DML and pretend the transaction is healthy
One failed row in allOrNone=true (default DML) rolls back that DML operation’s unit of work as documented—design user-facing messages accordingly
When Not to Use Triggers
Triggers are powerful and easy to overuse. Prefer not writing a trigger when:
- Requirement Prefer
- Stamp fields on the same record on save Before-save record-triggered Flow
- Simple related create/update with admin-owned logic After-save Flow
- Guided multi-screen user process Screen Flow
- Time-based daily criteria sweep Scheduled Flow or declarative schedule
- Roll-up that master-detail already supports Roll-up summary field
Point-and-click validation of field formats Validation rules (or before-save Flow/before trigger when rules are insufficient)
Use Apex triggers / invocable Apex when you need complex bulk algorithms, fine-grained recursion control, sophisticated collection processing, callout orchestration with maps, packaged logic that must be versioned in code, or scenarios the exam stem explicitly marks as unsuitable for declarative tools.
On “best solution” questions: if the stem stresses admin maintainability , no code , or simple field update , declarative wins. If the stem stresses bulk data loads , governor-safe complex related updates , or existing trigger frameworks , Apex patterns win.
Governor Mindset for Trigger Design
Triggers share the transaction’s limits with Flows, validation, workflow legacy steps, and roll-ups that fire in the same save. Design so your contribution is minimal:
- Fewest SOQL/DML statements possible (not fewest lines of code)
- Selective entry (change checks, handler short-circuits)
- Avoid updating the triggering record again in after context when before would suffice
- Do not query inside loops “just this once”—exam graders treat that as always wrong
- Putting Patterns Together
- One trigger per object → handler dispatch
- Handler checks recursion/bypass → calls service
- Service collects Ids → one query → Maps
- Service builds lists → one DML
- Prefer before field fixes; after only for true side effects
- Prefer Flow when declarative fully solves it
Internalize that checklist and most bulk-trigger exam items become mechanical.