Study guide
8.4 Recursion, Cascading & Transaction Control
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.4 Recursion, Cascading & Transaction Control
Key Takeaways Explain This Topic AI
Recursive trigger re-entry and cascading master-detail/roll-up updates multiply DML rows, SOQL, and CPU in one transaction—design selective logic and static guards
Static Boolean or Set<Id> recursion guards stop infinite loops; prefer Id sets when some records still need processing on re-entry
System.isBatch, isFuture, isQueueable, isScheduled help branch logic so interactive and async contexts do not double-apply side effects
Database.setSavepoint and rollback undo uncommitted work in the transaction; understand partial failure vs full rollback behavior
Mixed DML occurs when non-setup and setup objects are written in the same synchronous transaction—split with async or redesign the unit of work
Last updated: August 2026
8.4 Recursion, Cascading & Transaction Control
Quick Answer: Trigger recursion happens when your DML (or automation you cause) saves records that fire the same trigger again. Cascading master-detail and roll-up updates chain saves across objects. Control re-entry with static Boolean or Set<Id> guards , branch on System.isBatch / isFuture / isQueueable when async contexts should behave differently, use savepoints to roll back partial work intentionally, and avoid mixed DML by not updating setup and non-setup objects together in one synchronous transaction.
Section 8.2 introduced bulk patterns and a simple recursion flag. This section deepens failure modes the exam loves: infinite loops, parent/child storms, async double-fire, transaction rollback, and mixed DML exceptions.
How Recursion Starts
Common recursion loops:
- After-update trigger updates the same records → before/after update fires again
- After-update updates children → child trigger updates parent → parent trigger updates children
- Flow or workflow field update re-enters triggers (partial OOE re-entry)
- Roll-up updates parent → parent after logic updates children again
- Unselective logic runs on every re-entry even when business data did not meaningfully change
Without a guard, recursion ends when a governor limit is hit (CPU time, DML statements, DML rows) or a depth-related failure surfaces—rarely with a friendly business error.
- Static Boolean Guard public with sharing class CaseTriggerHandler {
- private static Boolean isHandling = false;
- public static void afterUpdate(List<Case> news, Map<Id, Case> oldMap) {
- if (isHandling) {
- return;
- }
- isHandling = true;
- try {
- CaseService.syncRelatedAccounts(news, oldMap);
- } finally {
- isHandling = false;
- }
- }
- }
Pros: Simple; stops all re-entrant runs of that method for the stack region you wrap.
Cons: Too coarse if legitimate bulk work needs a second pass for other records in a complex chain; resetting the flag in finally allows later separate operations in the same transaction (for example, a second independent DML from a different code path) to run again—which is often desired.
Place the guard at the handler boundary so all service work is covered. Guarding only one helper while another helper still re-enters leaves holes.
- Static Set<Id> Guard (Finer Control) public with sharing class OpportunityTriggerHandler {
- private static Set<Id> alreadyProcessed = new Set<Id>();
- public static void afterUpdate(List<Opportunity> news, Map<Id, Opportunity> oldMap) {
- List<Opportunity> firstTime = new List<Opportunity>();
- for (Opportunity o : news) {
- if (alreadyProcessed.contains(o.Id)) {
- continue;
- }
- alreadyProcessed.add(o.Id);
- firstTime.add(o);
- }
- if (firstTime.isEmpty()) {
- return;
- }
- OpportunityService.applyAfterUpdateSideEffects(firstTime, oldMap);
- }
- }
Use Id sets when:
- The same trigger may fire multiple times for overlapping Ids
- You still want to process new Ids that appear later in the transaction
- Boolean would incorrectly skip unrelated records
Remember static state lasts for the transaction (request), not forever across user sessions. Tests should reset or avoid depending on polluted statics between methods when isolation matters—use Test.startTest / stopTest carefully and design handlers to be test-friendly.
Cascading Master-Detail and Roll-Ups
Master-detail relationships cascade deletes and can cascade reparenting behavior depending on configuration. Roll-up summary fields on the master recalculate when detail records change, which updates the master and invokes the master’s order of execution.
- Amplification example
- 200 OpportunityLineItem updates in one chunk
- Each touches roll-ups on Opportunity (and possibly Account via further roll-ups)
- Parent Opportunity triggers, Flows, and validation run
- If parent logic updates each child again, row counts explode
- Mitigations
- Selective entry: only run heavy logic when watched fields change
- Aggregate in maps; update each parent once
- Prefer platform roll-ups over hand-written parent counters when they fit
- Move non-urgent fan-out to Queueable / batch so interactive saves stay light
- Avoid bidirectional “child updates parent updates child” without Id guards
- Async Context Awareness
Side effects that should not double-run in async re-processing check context: if (System.isFuture() || System.isQueueable()) {
- // skip enqueueing another future/queueable that would chain unboundedly
- return;
- }
- if (System.isBatch()) {
- // use bulk paths safe for large scopes; avoid per-row @future
- }
- Method True when
- System.isBatch() Running in batch Apex execute/context
- System.isFuture() Inside @future method
- System.isQueueable() Inside Queueable execute
- System.isScheduled() Scheduled Apex job context
- System.isQueueable() / combined checks Design async chains intentionally
- Exam patterns
- Do not call @future from a context that is already future (platform restriction)
- Batch + callouts need Database.AllowsCallouts and bulk design
Triggers that enqueue async work on every recursive pass can create job storms—combine recursion guards with async checks
Savepoints and Rollbacks
Apex can mark a savepoint and roll back DML to that point within the open transaction : Savepoint sp = Database.setSavepoint();
- try {
- insert primaryRecords;
- insert relatedRecords; // suppose this throws
- } catch (DmlException e) {
- Database.rollback(sp);
- // primaryRecords inserts are undone; transaction can continue or rethrow
- throw e; // often rethrow after cleanup/logging
- }
- Key facts
Rollback undoes DML after the savepoint; it does not undo static variable changes in memory—reset guards carefully if you continue the transaction
- Nested savepoints are possible; roll back to a specific savepoint
- Once the transaction commits , savepoints from that transaction are gone—no cross-request undo
Rolling back and then continuing requires clear product behavior so users are not shown success for undone work
Database methods with partial success ( allOrNone = false ) offer row-level error handling without necessarily using savepoints—choose the tool that matches the business need
Savepoints are for controlled compensation inside complex multi-step Apex, not a substitute for validation that should have blocked the save in before context.
Mixed DML and Setup Objects
Salesforce separates setup objects (for example Group, GroupMember, UserRole, some User DML scenarios, Permission Set assignments—consult current docs for the exact list) from non-setup sObjects (Account, Contact, custom business objects). Performing DML on both categories in the same synchronous transaction throws a mixed DML error. // Problematic pattern (illustrative):
- insert new Account(Name = 'Acme');
- insert new Group(Name = 'Acme Team', Type = 'Regular'); // setup-related
- // → Mixed DML exception risk
- Typical solutions
- Move one category’s DML to async ( @future , Queueable) so it runs in a separate transaction
- Restructure so a single interactive transaction only touches one category
- In tests, use System.runAs and patterns documented for setup object testing; isolate setup DML
Exam stems often show User/Permission Set work combined with Account inserts—recognize mixed DML and pick async split or redesign.
- Transaction Boundaries (Big Picture)
- One user save or one Apex request shares one set of governors until commit or hard failure
Uncaught exceptions roll back uncommitted work for that transaction (with nuances around email/async already enqueued—design assuming failure should not leave partial business state)
Test.startTest() / Test.stopTest() provide a fresh governor window for async in tests—not a production transaction split
Cascading automations stay inside the same transaction until commit—hence OOE + recursion + roll-ups all share limits
- Design Checklist for Stable Triggers
- Bulkify first (collect/query/map/DML once)
- Change-detect so re-entry has less work even if it occurs
- Add static Boolean or Set<Id> guards around after-context side effects
- Prefer before-context same-record edits to avoid self-updates
- Mind parent roll-up storms on master-detail
- Gate async enqueue with System.isFuture / isQueueable / isBatch
- Use savepoints only for intentional multi-step compensation
- Split setup vs non-setup DML across transactions
- Exam Scenario Drill
- Infinite loop symptoms (CPU timeout, too many DML) → missing recursion guard or after-update self-DML
- Works for one record, fails at 200 → not bulkified, not primarily a savepoint issue
- Mixed DML message → setup + non-setup in one sync transaction
- Logic ran in UI and again in batch → missing context checks or unselective statics
- Partial inserts visible after error → understand allOrNone vs partial Database methods vs rollback
Master recursion and transaction control and you close the loop on trigger design: correct context (8.1), bulk structure (8.2), OOE placement (8.3), and safe re-entry under load (8.4).