Skip to main content

SF-0335 · Scenario · Medium

How to avoid the recursive trigger?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

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 before trigger 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 @future or 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.