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 insertafter insertbefore updateafter 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
}
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
);
}
- 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;
}
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;
}
}
- Prevents recursive execution
- Centralized control
Step 5 Only Update Records When Data Actually Changes
BAD
acc.Status__c = 'Active';
Runs every time → recursion.
GOOD
if (acc.Status__c != 'Active') {
acc.Status__c = 'Active';
}
- 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
);
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;
- No duplicates
- Bulk-safe
Step 7 Use before Triggers Instead of after When Possible
BAD
after update → update same record
GOOD
before update → modify Trigger.new
if (acc.Score__c == null) {
acc.Score__c = 0;
}
- 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>();
}
if (GlobalTriggerState.executed.contains('Account')) return;
GlobalTriggerState.executed.add('Account');
- 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;
}
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.

