The standard fix for a recursive trigger is a static boolean variable. Apex statics live for the lifetime of a single transaction — perfect for “have I already run in this transaction?” guards. Set the flag on first entry, check it on every entry, and return early if it’s already true.
The minimal pattern
public class AccountTriggerHelper {
public static Boolean alreadyRan = false;
}
trigger AccountTrigger on Account (after update) {
if (AccountTriggerHelper.alreadyRan) return;
AccountTriggerHelper.alreadyRan = true;
// ... your real logic, including any same-object DML
}
The second invocation — fired by the DML inside the trigger — sees alreadyRan == true and exits immediately. The flag resets at the end of the transaction because static state doesn’t survive across transactions.
A per-record guard (more precise)
A blanket boolean blocks legitimate re-entry too. A per-record Set<Id> guard is finer-grained:
public class AccountTriggerHelper {
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 (!AccountTriggerHelper.processedIds.contains(a.Id)) {
toProcess.add(a);
AccountTriggerHelper.processedIds.add(a.Id);
}
}
if (toProcess.isEmpty()) return;
// ... operate on toProcess only
}
This lets the trigger still process other accounts updated later in the same transaction, while skipping the records it has already touched.
Better: do less to begin with
The cleanest fix is often to not write back what you just wrote:
- Move the calculation into a
beforetrigger so the field update is part of the same save — no second DML. - If you must update in
after, compare new vs old and only update records whose calculated field actually changed. If nothing’s different, no DML, no recursion.
List<Account> toUpdate = new List<Account>();
for (Account a : Trigger.new) {
Decimal newScore = a.AnnualRevenue == null ? 0 : a.AnnualRevenue / 1000;
if (newScore != a.Health_Score__c) {
toUpdate.add(new Account(Id = a.Id, Health_Score__c = newScore));
}
}
if (!toUpdate.isEmpty()) update toUpdate;
In a trigger framework
Mature frameworks (fflib, kevinohara80/sfdc-trigger-framework, Hari Krishnan’s) have built-in recursion handling. The dispatcher tracks per-(object, context) entry counts and exposes a hook like TriggerHandler.preventRecursion().
Common interview follow-ups
- Why does a static variable work? — Apex statics are scoped to a transaction. Re-entry on the same trigger sees the same static state; a fresh request gets a fresh value.
- What about
@futureor batch jobs? — Each async job is a new transaction with its own statics. The guard doesn’t carry over. - Can I use a custom setting instead? — You could, but you’d pay a SOQL query on every trigger fire. Stick with the static.
Verified against: Apex Developer Guide — Static and Instance Variables, Triggers — Avoiding Recursion. Last reviewed 2026-05-17.