Skip to content
Skip to content
SalesforceFox

SalesforceFox

Easy way Learning & Exploring Salesforce !

  • Home
  • All Blogs
  • How To
  • Tips & Tricks
  • Salesforce Quiz
  • Resources
  • About
Best way to avoid Recursion in Apex Triggers

Best way to avoid Recursion in Apex Triggers

Piyush LakhaniApril 17, 2025April 17, 2025

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.

Table of Contents
[Open][Close]
  • Commonly used methods & its limitations
    • Method 1 : Using Static Boolean Variable
    • Method 2 : Using Static Set to track Processed Records
    • Method 3 : Using Static Map of trigger event Key
  • The Best way to avoid recursion
    • Method 4 : Using Centralized Static Map with Trigger OperationType
  • All in one comparison of Methods
  • Summary

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 static Map<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.operationType to 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.

Method
Record-Level control
Trigger Context Aware(Before/After)
Bulk-Safe
Cross-Trigger Support
1
❌ No
❌ No
❌ No
❌ No
2
✅ Yes
❌ No
❌ No
❌ No
3
❌ No
✅ Yes
✅ Yes
✅ Yes
4
✅ Yes
✅ Yes
✅ Yes
✅ Yes

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.

Share this Now :

Related Posts:

  • Mastering SOLID principles in Apex
    Mastering SOLID Principles in Apex: Write Cleaner,…
  • Create Dynamic Job Scheduling Framework in Salesforce
    Create a Dynamic Job Scheduling Framework in…
  • How to use Cron Expressions in Salesforce
    How to use Cron Expressions in Salesforce - Discover…
  • Add Custom Toast Messages In Screen Flows In Salesforce
    Make Salesforce Screen flows more Interactive with…
Best Practices, Development, Trigger

Post navigation

Previous: Send Automatic Reminder Emails to Inactive Users in Salesforce
Next: 12 Must-know Javascript Concepts for LWC : A guide for Salesforce Developers

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Get Weekly Salesforce Quiz

Categories

  • Admin (8)
  • Apex (6)
  • Automation (7)
  • Best Practices (6)
  • Flow (5)
  • LWC (4)
  • Security (1)
  • SOQL (1)
  • Trigger (1)
  • Uncategorized (1)

Recent Posts

  • User Access Policy in Salesforce : The Smartest way to Automate User Management Jobs
  • Display Apex-Defined Collection Data directly into Screen Flow Data Table
  • How to Secure Your Salesforce Org: First Steps Against Cyber Attacks and Social Engineering
  • Make Salesforce Screen flows more Interactive with Toast Messages
  • How to Zip & Unzip files Natively in Salesforce: Use This Ready-to-Use ZIP Utility

Tags

Admin Apex Aura Component Automation Best Practices Development Email Alerts Flow Flow Approval Process How To Lightning Experience LWC Reports Scheduled Jobs Security SOQL Tips Trigger User Management

Archives

  • November 2025 (1)
  • October 2025 (1)
  • August 2025 (1)
  • July 2025 (1)
  • June 2025 (2)
  • May 2025 (1)
  • April 2025 (2)
  • March 2025 (2)
  • January 2025 (2)
  • December 2024 (1)
  • October 2024 (1)
  • September 2024 (1)
  • August 2024 (1)
  • June 2024 (1)
  • April 2024 (1)
  • March 2024 (1)
  • October 2023 (1)
  • Linkedin
  • Youtube
  • Privacy Policy
  • Terms of Use
  • Contact
Theme: BlockWP by Candid Themes.
© 2023-2026 | SalesforceFox.com