Trigger Logic Causing Recursive Updates or Data Duplication
Selavina B

Selavina B @selavina_b_de3b87f13c96a6

About: Salesforce expert with 12 years of experience in Sales, Service & Marketing Cloud. I build scalable Salesforce solutions and share practical dev tips, tutorials, and real-world use cases.

Joined:
Dec 9, 2025

Trigger Logic Causing Recursive Updates or Data Duplication

Publish Date: Jan 8
0 0

Problem Statement: Poorly designed Apex triggers run multiple times per transaction, causing recursive updates, duplicate records, or unintended field changes due to missing recursion guards, mixed responsibilities, or lack of a trigger framework.

Salesforce Trigger Logic Causing Recursive Updates or Data Duplication
Step-by-Step Fix (with Code)
Why This Happens in Salesforce
Salesforce triggers can run multiple times in a single transaction:

  • before insert
  • after insert
  • before update
  • after update

Recursion and duplication occur when:

  • A trigger updates the same object it fires on
  • Multiple triggers handle the same logic
  • No recursion guard is used
  • Logic is scattered across trigger contexts

Common Symptoms

  • Duplicate child records created
  • Fields updated repeatedly
  • CPU time limit exceeded
  • “Too many DML statements” errors
  • Records updated even when values didn’t change

Step 1 Identify the BAD trigger pattern
WRONG: Updating records inside after update without guard

trigger AccountTrigger on Account (after update) {
    for (Account acc : Trigger.new) {
        acc.Description = 'Updated';
    }
    update Trigger.new; //  Recursive trigger
}

Enter fullscreen mode Exit fullscreen mode

This re-fires the trigger endlessly.

Step 2 Move logic to a Trigger Handler Framework
Trigger (Single Responsibility)

trigger AccountTrigger on Account (
    before insert, before update,
    after insert, after update
) {
    AccountTriggerHandler.run(
        Trigger.operationType,
        Trigger.new,
        Trigger.oldMap
    );
}

Enter fullscreen mode Exit fullscreen mode
  • One trigger per object
  • No logic inside trigger

Step 3 Add a Recursion Guard (Critical)
Static Boolean Guard

public class TriggerControl {
    public static Boolean isRunning = false;
}
Enter fullscreen mode Exit fullscreen mode

Step 4 Implement Guarded Logic in Handler

public class AccountTriggerHandler {

    public static void run(
        System.TriggerOperation operation,
        List<Account> newList,
        Map<Id, Account> oldMap
    ) {
        if (TriggerControl.isRunning) return;
        TriggerControl.isRunning = true;

        if (operation == System.TriggerOperation.BEFORE_UPDATE) {
            beforeUpdate(newList, oldMap);
        }

        if (operation == System.TriggerOperation.AFTER_INSERT) {
            afterInsert(newList);
        }

        TriggerControl.isRunning = false;
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Prevents recursive execution
  • Centralized control

Step 5 Only Update Records When Data Actually Changes
BAD

acc.Status__c = 'Active';
Enter fullscreen mode Exit fullscreen mode

Runs every time → recursion.

GOOD

if (acc.Status__c != 'Active') {
    acc.Status__c = 'Active';
}
Enter fullscreen mode Exit fullscreen mode
  • Prevents unnecessary updates
  • Reduces trigger re-fires

Step 6 Never Insert Child Records Without Duplication Checks
BAD (duplicates)

insert new Contact(
    LastName = 'Auto',
    AccountId = acc.Id
);
Enter fullscreen mode Exit fullscreen mode

GOOD (check existing records)

Map<Id, Integer> contactCountMap = new Map<Id, Integer>();

for (AggregateResult ar : [
    SELECT AccountId accId, COUNT(Id) cnt
    FROM Contact
    WHERE AccountId IN :accountIds
    GROUP BY AccountId
]) {
    contactCountMap.put((Id) ar.get('accId'), (Integer) ar.get('cnt'));
}

List<Contact> contactsToInsert = new List<Contact>();
for (Account acc : newList) {
    if (!contactCountMap.containsKey(acc.Id)) {
        contactsToInsert.add(
            new Contact(LastName = 'Auto', AccountId = acc.Id)
        );
    }
}
insert contactsToInsert;
Enter fullscreen mode Exit fullscreen mode
  • No duplicates
  • Bulk-safe

Step 7 Use before Triggers Instead of after When Possible
BAD

after update → update same record
Enter fullscreen mode Exit fullscreen mode

GOOD

before update → modify Trigger.new

Enter fullscreen mode Exit fullscreen mode
if (acc.Score__c == null) {
    acc.Score__c = 0;
}

Enter fullscreen mode Exit fullscreen mode
  • No DML
  • No recursion

Step 8 Split Logic by Responsibility
BAD

  • Validation
  • Field updates
  • Record creation
  • Callouts All in one trigger.

GOOD Architecture

  • AccountTriggerHandler
  • AccountService
  • AccountValidator
  • AccountAsyncProcessor
  • Easier testing
  • No duplicate execution

Step 9 Prevent Cross-Object Trigger Loops
COMMON LOOP

  • Account trigger updates Contact
  • Contact trigger updates Account

  • FIX Shared Static Guard

public class GlobalTriggerState {
    public static Set<String> executed = new Set<String>();
}
Enter fullscreen mode Exit fullscreen mode
if (GlobalTriggerState.executed.contains('Account')) return;
GlobalTriggerState.executed.add('Account');
Enter fullscreen mode Exit fullscreen mode
  • Prevents infinite cross-object loops

Step 10 Final Safe Trigger Template (Production-Ready)

trigger AccountTrigger on Account (before update, after insert) {
    if (TriggerControl.isRunning) return;
    TriggerControl.isRunning = true;

    if (Trigger.isBefore && Trigger.isUpdate) {
        for (Account acc : Trigger.new) {
            if (acc.Status__c != 'Active') {
                acc.Status__c = 'Active';
            }
        }
    }

    if (Trigger.isAfter && Trigger.isInsert) {
        AccountService.createDefaultContacts(Trigger.new);
    }

    TriggerControl.isRunning = false;
}

Enter fullscreen mode Exit fullscreen mode

Conclusion
In Salesforce Development, recursive triggers and data duplication are not platform bugs they are design issues. The fix is disciplined trigger architecture: one trigger per object, handler classes, recursion guards, before-trigger updates, and strict duplication checks. When triggers are treated as event routers instead of logic containers, recursion stops, data stays clean, and your org becomes stable and scalable.

Comments 0 total

    Add comment