In Salesforce, Trigger recursion occurs when a trigger causes a DML operations (like insert, update), which in turn causes the same trigger to run again. This creates a loop that can lead to performance problems or governor limit exceptions in salesforce. Here, we will explore The Best way to avoid recursion in Apex Triggers.
Commonly used methods & its limitations
First of all, Let’s see some commonly used methods to prevent trigger recursion on salesforce.
Method 1 : Using Static Boolean Variable
Using Static boolean variable is most common and simple method. We can use static boolean flag to check if trigger has already been executed during the transaction. Let’s see blow code
public class AccountTriggerHelper {
public static Boolean triggerProcessed = false;
public static void handleBeforeUpdate(List<Account> newList, Map<Id, Account> oldMap) {
if (triggerProcessed) {
return;
}
triggerProcessed = true;
for (Account acc : newList) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.Name != oldAcc.Name) {
acc.Description = 'Account description updated!';
}
}
}
}Limitations of this method:
- It doesn’t have record level or trigger-context level control
- It will work only up to 200 records.
- Use of static Boolean is considered Anti-Pattern & not recommended by Salesforce.
Method 2 : Using Static Set to track Processed Records
In this method, we can use Set<Id> to store ids of processed records and then check each record before processing it further in trigger.
public class ContactTriggerHelper {
private static Set<Id> processedContacts = new Set<Id>();
public static void handleAfterInsert(List<Contact> newContacts) {
List<Contact> toUpdate = new List<Contact>();
for (Contact con : newContacts) {
if (!processedContacts.contains(con.Id)) {
con.Description = 'Processed!';
toUpdate.add(con);
processedContacts.add(con.Id); // add record Id once processed !
}
}
if (!toUpdate.isEmpty()) {
update toUpdate; // This could retrigger the trigger,
// so the Set helps avoid reprocessing.
}
}
}Limitations of this method:
- It doesn’t distinguish between trigger context like
before update,after insert, etc. so it will not process a record even if it is for different trigger context. - It’s not reusable since you need separate
Set<Id>variables per object. - It can not handle bulk records due to risk of heap space.
Method 3 : Using Static Map of trigger event Key
In this method, We create separate TriggerController which stores Static Map<String, Boolean> to store trigger event key and its boolean value. e.g. Once in After Update Lead Trigger, we store custom key ‘Lead_afterUpdate’ and its value as True in map. Let’s see its usage here,
public class TriggerController {
private static Map<String, Boolean> triggerRunMap = new Map<String, Boolean>();
public static Boolean shouldRun(String key) {
if (triggerRunMap.containsKey(key) && triggerRunMap.get(key)) {
return false;
} else {
triggerRunMap.put(key, true);
return true;
}
}
}trigger OpportunityTrigger on Opportunity (after insert) {
if (TriggerController.shouldRun('Lead_afterUpdate')) {
OpportunityHelper.processAfterInsert(Trigger.new);
}
}As seen in above trigger, before calling Trigger helper method, we check if this method already run before or not using TriggerController’s shouldRun method.
Limitations of this method:
- This method is for controlling whether a block of logic runs, not whether a specific record is processed.
- If a block is executed twice due to nested DML on different objects, this may not catch it.
- Since event Key is user-defined string, it can create confusion when multiple devs working on it.
The Best way to avoid recursion
Method 4 : Using Centralized Static Map with Trigger OperationType
In this method, We will create separate Utility Class called TriggerRecursionManager.
- In
TriggerRecursionManager, we will create staticMap<TriggerOperation, Set<Id>>which remembers data across the transaction. - It keeps track of which record IDs have been processed per trigger operation (
BEFORE_INSERT,AFTER_UPDATE, etc). - while using this, we need to use
Trigger.operationTypeto detect different trigger operations.
As seen in below class, we will create a method called shouldProcess which accepts TriggerOperation and record to process from trigger context.
It will check if this record has already been processed with current trigger operation, if so, it will return False, otherwise it will store trigger operation and record id in static Map and return True
Therefore, next time same record can’t be processed for same operation.
public class TriggerRecursionManager {
private static Map<TriggerOperation, Set<Id>> processedIdsByContext = new Map<TriggerOperation, Set<Id>>();
// Method to check if record is already processed for a trigger operation
public static Boolean shouldProcess(TriggerOperation op, Id recordId) {
if (!processedIdsByContext.containsKey(op)) {
processedIdsByContext.put(op, new Set<Id>());
}
Set<Id> processedIds = processedIdsByContext.get(op);
if (processedIds.contains(recordId)) {
return false; // Already processed in this trigger operation
}
processedIds.add(recordId); // Mark as processed for this operations
return true;
}
}Now, use this shouldProcess method in Trigger helper class before we perform any logic on records. Make sure to add this method on Helper class, Not directly on trigger itself to keep code cleaner and modular.
public class AccountTriggerHelper {
public static void handleBeforeUpdate(List<Account> updateAccs, Map<Id,Account> OldAccounts){
for(Account ac : updateAccs){
//check first if this record has already been processed earlier in beforeUpdate
if (TriggerRecursionManager.shouldProcess(Trigger.operationType, ac.Id)) {
ac.Description = 'Account description updated !' ;
}
}
}
}Why this method Wins in All Scenarios to avoid Trigger Recursion:
- It’s allows per-record, per trigger-context control.
- It’s centralized logic since recursion logic lives only on Utility class.
- It’s highly reusable for any objects, because we use just one line of code inside any handler to guard main logic.
- it works perfectly with bulk data (200+ records at once).
- it’s flexible so it works even if multiple trigger updating same records.
All in one comparison of Methods
Let’s see overall comparison of all method considering various factors.
Summary
As We have seen here, we can use multiple methods to prevent Trigger Recursion. It’s important to understand what works best for us as per our requirements. However, The Best way to avoid Recursion in Apex Triggers is using a method in which we utility class TriggerRecursionManager as shown above. Apart of from choosing correct method, Let’s make sure we follow best practices while implementing triggers.




