Trigger recursion is the classic Salesforce footgun: your trigger updates a field on the same object, the update fires the trigger again, and the cycle continues until the platform throws Maximum trigger depth exceeded. The fix is a static guard variable in a helper class — static state lives for exactly one Apex transaction, which is exactly the scope of the recursion problem.
The 60-second answer
Create a helper class with a public static Boolean alreadyRun = false; (or public static Set<Id> processedIds = new Set<Id>(); for finer-grained control). At the top of the trigger logic, check the flag. If it’s set, return. Otherwise, set it and proceed. The Set-based version is better for bulk updates where some records have been processed and others haven’t in the same transaction.
The recursion that needs fixing
trigger AccountTrigger on Account (after update) {
List<Account> toUpdate = new List<Account>();
for (Account a : Trigger.new) {
a.Touched_Count__c = (a.Touched_Count__c == null ? 0 : a.Touched_Count__c) + 1;
toUpdate.add(a);
}
update toUpdate; // ← fires the trigger again — infinite loop
}
The runtime kills this at recursion depth 16 with Maximum trigger depth exceeded. Even before that, every iteration burns governor limits — SOQL queries, DML statements, CPU time.
Fix 1 — Boolean guard
Simplest pattern. Works when “run once per transaction” is correct.
public class AccountTriggerHandler {
public static Boolean alreadyRun = false;
}
trigger AccountTrigger on Account (after update) {
if (AccountTriggerHandler.alreadyRun) return;
AccountTriggerHandler.alreadyRun = true;
List<Account> toUpdate = new List<Account>();
for (Account a : Trigger.new) {
Account clone = new Account(Id = a.Id);
clone.Touched_Count__c = (a.Touched_Count__c == null ? 0 : a.Touched_Count__c) + 1;
toUpdate.add(clone);
}
update toUpdate;
}
Static variables are transaction-scoped — they reset between user requests. Inside one transaction (one trigger chain), they persist. That’s the property we exploit.
Fix 2 — Per-record Set guard
Use this when bulk updates have a mix of records that should be processed and ones that have already been processed in this transaction.
public class AccountTriggerHandler {
public static Set<Id> processedIds = new Set<Id>();
}
trigger AccountTrigger on Account (after update) {
List<Account> toProcess = new List<Account>();
for (Account a : Trigger.new) {
if (!AccountTriggerHandler.processedIds.contains(a.Id)) {
AccountTriggerHandler.processedIds.add(a.Id);
toProcess.add(a);
}
}
if (toProcess.isEmpty()) return;
List<Account> toUpdate = new List<Account>();
for (Account a : toProcess) {
Account clone = new Account(Id = a.Id);
clone.Touched_Count__c = (a.Touched_Count__c == null ? 0 : a.Touched_Count__c) + 1;
toUpdate.add(clone);
}
update toUpdate;
}
This is the more robust pattern. Use it as a default unless you’re certain the Boolean is enough.
Fix 3 — Diff against old values (best when applicable)
If the recursion is driven by re-detecting the same change the second time around, the cleanest fix isn’t a guard — it’s detecting that the change has already been made:
trigger AccountTrigger on Account (after update) {
List<Account> toUpdate = new List<Account>();
for (Account a : Trigger.new) {
Account oldA = Trigger.oldMap.get(a.Id);
// Only act if the field that *triggers* this logic actually changed
if (a.Status__c != oldA.Status__c && a.Status__c == 'Active') {
Account clone = new Account(Id = a.Id);
clone.Activated_Date__c = Date.today();
toUpdate.add(clone);
}
}
if (!toUpdate.isEmpty()) update toUpdate;
}
On the recursive pass, a.Status__c and oldA.Status__c are both 'Active' — the change condition is false, no further update, no infinite loop. This pattern is the most defensive because it works even if some future developer removes the static guard.
Why static variables work (and their limits)
Static variables in Apex are scoped to the execution context — one synchronous request, one batch chunk, one queueable execution, etc. They reset between:
- Two separate user clicks
- Two separate API calls
- Each batch chunk in
Database.Batchable - Each queueable invocation when chained
- Each scheduled execution
They persist within:
- A single trigger chain (parent triggers grandchild updates triggers grandchild updates…)
- A single Flow/Process Builder invocation
- The DML cascade from one user save
The static-guard pattern works because we want exactly the single-transaction scope.
The Trigger Framework pattern
Real production orgs put the guard inside a Trigger Framework — a base class that every trigger handler inherits from. Frameworks (fflib, kevinohara80/sfdc-trigger-framework, etc.) hand-wave the recursion guard for you via an isExecuting flag in the base class.
public abstract class TriggerHandler {
private static Map<String, Integer> loopCountMap = new Map<String, Integer>();
protected Boolean isRecursive() {
String handlerName = String.valueOf(this).split(':')[0];
Integer count = loopCountMap.get(handlerName);
if (count != null && count >= 1) return true;
loopCountMap.put(handlerName, (count == null ? 1 : count + 1));
return false;
}
}
In the interview, mention you’d use a framework — even better, name the one your last team used.
Anti-patterns
- Boolean guard in the trigger file itself — works once but the next person who edits the trigger may not know it’s there. Always put the guard in a separate handler class.
- Re-initializing the static variable inside the trigger —
Boolean alreadyRun = false;(nostatic) defeats the whole pattern. - Trying to use
Statefulon a Database.Batchable for trigger recursion — wrong tool. Static variables are the right scope. - Catching
LimitExceptionand ignoring — masks the bug, eats CPU time anyway. - Setting the flag in a
beforetrigger and forgetting it propagates toafter— bothbeforeandafterrun in the same transaction, so the flag will persist. That’s usually what you want, but be conscious of it.
How to answer in 30 seconds
“Recursion happens when a trigger does DML on the same object and re-fires itself. Fix it with a public static Boolean alreadyRun in a separate handler class — static state lives for the transaction, exactly the scope we need. For finer control, use a Set<Id> processedIds. And whenever possible, compare Trigger.new vs Trigger.old so the logic only fires when the change condition is actually true.”
How to answer in 2 minutes
Show the Set-based guard pattern (more robust than Boolean), explain why static variables are the right scope (transaction-scoped, persist across the trigger chain), mention that real orgs use a Trigger Framework with the guard built in, and give the strongest version: diff Trigger.new against Trigger.oldMap so the trigger naturally short-circuits on the recursive pass.
Likely follow-up questions
- What’s the maximum trigger depth Salesforce allows?
- Does the static variable reset between batch chunks?
- How is recursion different in
beforevsaftertriggers? - What’s the trigger order of execution and where does recursion fit?
- Have you used a Trigger Framework? Which one and why?
Verified against: Apex Developer Guide — Triggers and Order of Execution, Static Variables. Last reviewed 2026-05-19.