Triggers & Trigger Frameworks
📖 Concept
Triggers are the primary way to execute custom logic when records are created, updated, deleted, or undeleted. Mastering triggers — including when they fire, how to structure them, and how to prevent common pitfalls — is essential for every Salesforce developer.
Trigger events:
before insert — Before new records are saved (no Id yet)
before update — Before changed records are saved
before delete — Before records are deleted
after insert — After new records are saved (Id available)
after update — After changed records are saved
after delete — After records are deleted
after undelete — After records are restored from Recycle Bin
Before vs After — when to use which:
- Before triggers: Modify the record's own fields (no DML needed — changes are auto-saved). Validate records (use addError to block save).
- After triggers: Access the record's Id (available after save). Create/update RELATED records. Make callouts (with @future). Fire platform events.
Trigger context variables:
Trigger.new — List of new record versions (insert/update)
Trigger.old — List of old record versions (update/delete)
Trigger.newMap — Map<Id, SObject> of new versions
Trigger.oldMap — Map<Id, SObject> of old versions
Trigger.isInsert — True during insert
Trigger.isUpdate — True during update
Trigger.isDelete — True during delete
Trigger.isBefore — True during before events
Trigger.isAfter — True during after events
Trigger.size — Number of records in the batch
The one-trigger-per-object rule: In enterprise orgs, you should have exactly ONE trigger per object that delegates to a handler class. Multiple triggers on the same object lead to unpredictable execution order and debugging nightmares.
Trigger frameworks solve common problems:
- Single entry point — One trigger per object
- Recursion prevention — Stop infinite loops
- Bypass mechanism — Skip triggers during data migration
- Testability — Business logic is in classes, not triggers
- Separation of concerns — Thin triggers, fat handlers
💻 Code Example
1// Complete Trigger Framework Implementation23// 1. ITriggerHandler Interface4public interface ITriggerHandler {5 void beforeInsert(List<SObject> newRecords);6 void beforeUpdate(List<SObject> newRecords, Map<Id, SObject> oldMap);7 void beforeDelete(List<SObject> oldRecords);8 void afterInsert(List<SObject> newRecords);9 void afterUpdate(List<SObject> newRecords, Map<Id, SObject> oldMap);10 void afterDelete(List<SObject> oldRecords);11 void afterUndelete(List<SObject> newRecords);12 Boolean isDisabled();13}1415// 2. TriggerDispatcher — Routes trigger events to handlers16public class TriggerDispatcher {1718 public static void run(ITriggerHandler handler) {19 // Check if handler is disabled (bypass)20 if (handler.isDisabled()) return;2122 // Route to appropriate method23 if (Trigger.isBefore) {24 if (Trigger.isInsert) handler.beforeInsert(Trigger.new);25 if (Trigger.isUpdate) handler.beforeUpdate(Trigger.new, Trigger.oldMap);26 if (Trigger.isDelete) handler.beforeDelete(Trigger.old);27 }28 if (Trigger.isAfter) {29 if (Trigger.isInsert) handler.afterInsert(Trigger.new);30 if (Trigger.isUpdate) handler.afterUpdate(Trigger.new, Trigger.oldMap);31 if (Trigger.isDelete) handler.afterDelete(Trigger.old);32 }33 }34}3536// 3. TriggerHandlerBase — Default (empty) implementations37public virtual class TriggerHandlerBase implements ITriggerHandler {38 // Bypass mechanism using Custom Metadata or static variable39 private static Set<String> disabledHandlers = new Set<String>();4041 public static void disableHandler(String handlerName) {42 disabledHandlers.add(handlerName);43 }4445 public static void enableHandler(String handlerName) {46 disabledHandlers.remove(handlerName);47 }4849 public virtual Boolean isDisabled() {50 return disabledHandlers.contains(51 String.valueOf(this).split(':')[0]52 );53 }5455 // Default empty implementations — override only what you need56 public virtual void beforeInsert(List<SObject> newRecords) {}57 public virtual void beforeUpdate(List<SObject> newRecords, Map<Id, SObject> oldMap) {}58 public virtual void beforeDelete(List<SObject> oldRecords) {}59 public virtual void afterInsert(List<SObject> newRecords) {}60 public virtual void afterUpdate(List<SObject> newRecords, Map<Id, SObject> oldMap) {}61 public virtual void afterDelete(List<SObject> oldRecords) {}62 public virtual void afterUndelete(List<SObject> newRecords) {}63}6465// 4. Concrete Handler — Opportunity66public class OpportunityTriggerHandler extends TriggerHandlerBase {6768 // Recursion guard69 private static Boolean hasRunAfterUpdate = false;7071 public override void beforeInsert(List<SObject> newRecords) {72 List<Opportunity> opps = (List<Opportunity>) newRecords;73 OpportunityService.setDefaults(opps);74 OpportunityService.validateAmounts(opps);75 }7677 public override void beforeUpdate(List<SObject> newRecords, Map<Id, SObject> oldMap) {78 List<Opportunity> opps = (List<Opportunity>) newRecords;79 Map<Id, Opportunity> oldOpps = (Map<Id, Opportunity>) oldMap;80 OpportunityService.validateStageTransitions(opps, oldOpps);81 }8283 public override void afterUpdate(List<SObject> newRecords, Map<Id, SObject> oldMap) {84 if (hasRunAfterUpdate) return; // Prevent recursion85 hasRunAfterUpdate = true;8687 List<Opportunity> opps = (List<Opportunity>) newRecords;88 Map<Id, Opportunity> oldOpps = (Map<Id, Opportunity>) oldMap;8990 // Only process opportunities that changed stage91 List<Opportunity> stageChanged = new List<Opportunity>();92 for (Opportunity opp : opps) {93 if (opp.StageName != oldOpps.get(opp.Id).StageName) {94 stageChanged.add(opp);95 }96 }9798 if (!stageChanged.isEmpty()) {99 OpportunityService.notifyStageChange(stageChanged);100 OpportunityService.updateAccountRevenue(stageChanged);101 }102 }103}104105// 5. The Trigger (thin — just 1 line of logic)106// trigger OpportunityTrigger on Opportunity107// (before insert, before update, after insert, after update, after delete) {108// TriggerDispatcher.run(new OpportunityTriggerHandler());109// }110111// 6. Service Class — Reusable business logic112public class OpportunityService {113114 public static void setDefaults(List<Opportunity> opps) {115 for (Opportunity opp : opps) {116 if (opp.StageName == null) opp.StageName = 'Prospecting';117 if (opp.CloseDate == null) opp.CloseDate = Date.today().addDays(90);118 if (opp.Probability == null) opp.Probability = 10;119 }120 }121122 public static void validateAmounts(List<Opportunity> opps) {123 for (Opportunity opp : opps) {124 if (opp.Amount != null && opp.Amount < 0) {125 opp.Amount.addError('Opportunity amount cannot be negative');126 }127 if (opp.Amount != null && opp.Amount > 10000000) {128 opp.addError('Opportunities over $10M require VP approval');129 }130 }131 }132133 public static void validateStageTransitions(134 List<Opportunity> newOpps, Map<Id, Opportunity> oldMap135 ) {136 // Prevent backward stage transitions137 Map<String, Integer> stageOrder = new Map<String, Integer>{138 'Prospecting' => 1, 'Qualification' => 2,139 'Needs Analysis' => 3, 'Proposal' => 4,140 'Negotiation' => 5, 'Closed Won' => 6, 'Closed Lost' => 6141 };142143 for (Opportunity opp : newOpps) {144 Opportunity oldOpp = oldMap.get(opp.Id);145 Integer newOrder = stageOrder.get(opp.StageName);146 Integer oldOrder = stageOrder.get(oldOpp.StageName);147148 if (newOrder != null && oldOrder != null && newOrder < oldOrder) {149 if (oldOpp.StageName != 'Closed Won' && oldOpp.StageName != 'Closed Lost') {150 opp.addError('Cannot move stage backward from ' +151 oldOpp.StageName + ' to ' + opp.StageName);152 }153 }154 }155 }156157 public static void notifyStageChange(List<Opportunity> changedOpps) {158 // Create tasks for sales managers159 List<Task> notifications = new List<Task>();160 for (Opportunity opp : changedOpps) {161 notifications.add(new Task(162 WhatId = opp.Id,163 Subject = 'Opportunity stage changed to: ' + opp.StageName,164 OwnerId = opp.OwnerId,165 ActivityDate = Date.today(),166 Priority = opp.Amount > 100000 ? 'High' : 'Normal'167 ));168 }169 if (!notifications.isEmpty()) insert notifications;170 }171172 public static void updateAccountRevenue(List<Opportunity> opps) {173 // Delegate to Queueable if complex174 Set<Id> accountIds = new Set<Id>();175 for (Opportunity opp : opps) {176 if (opp.AccountId != null) accountIds.add(opp.AccountId);177 }178 if (!accountIds.isEmpty()) {179 System.enqueueJob(new AccountRevenueCalculator(accountIds));180 }181 }182}
🏋️ Practice Exercise
Trigger Framework Practice:
- Implement the complete trigger framework (interface, dispatcher, base class) from scratch
- Create a concrete handler for the Account object that validates, sets defaults, and updates related records
- Add a bypass mechanism using Custom Metadata Type (Trigger_Setting__mdt) instead of static variables
- Write comprehensive test classes for your trigger handler covering all 7 trigger events
- Implement a recursion guard that prevents infinite trigger loops
- Build a trigger handler that tracks field changes: "which fields changed and what were the old values?"
- Create a trigger on Case that automatically escalates based on Priority and Age
- Test your framework with a Data Loader operation of 1000 records
- Add logging to your framework that records which handlers executed and how long each took
- Implement a before-trigger that prevents deletion of records with specific criteria
⚠️ Common Mistakes
Having multiple triggers on the same object — execution order is NOT guaranteed. Use one trigger per object with a handler framework
Putting business logic directly in the trigger file — logic should be in handler/service classes for testability and reusability
Not handling recursion — trigger A updates object B, trigger B updates object A → infinite loop → governor limit exception
Modifying Trigger.new records in after triggers — after triggers have read-only records. Use before triggers to modify the triggering record's fields
Not checking which fields changed in update triggers — processing ALL updated records when only a subset actually changed wastes resources
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Triggers & Trigger Frameworks. Login to unlock this feature.