Triggers & Trigger Frameworks

0/9 in this phase0/41 across the roadmap

📖 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:

  1. Single entry point — One trigger per object
  2. Recursion prevention — Stop infinite loops
  3. Bypass mechanism — Skip triggers during data migration
  4. Testability — Business logic is in classes, not triggers
  5. Separation of concerns — Thin triggers, fat handlers

💻 Code Example

codeTap to expand ⛶
1// Complete Trigger Framework Implementation
2
3// 1. ITriggerHandler Interface
4public 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}
14
15// 2. TriggerDispatcher — Routes trigger events to handlers
16public class TriggerDispatcher {
17
18 public static void run(ITriggerHandler handler) {
19 // Check if handler is disabled (bypass)
20 if (handler.isDisabled()) return;
21
22 // Route to appropriate method
23 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}
35
36// 3. TriggerHandlerBase — Default (empty) implementations
37public virtual class TriggerHandlerBase implements ITriggerHandler {
38 // Bypass mechanism using Custom Metadata or static variable
39 private static Set<String> disabledHandlers = new Set<String>();
40
41 public static void disableHandler(String handlerName) {
42 disabledHandlers.add(handlerName);
43 }
44
45 public static void enableHandler(String handlerName) {
46 disabledHandlers.remove(handlerName);
47 }
48
49 public virtual Boolean isDisabled() {
50 return disabledHandlers.contains(
51 String.valueOf(this).split(':')[0]
52 );
53 }
54
55 // Default empty implementations — override only what you need
56 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}
64
65// 4. Concrete Handler — Opportunity
66public class OpportunityTriggerHandler extends TriggerHandlerBase {
67
68 // Recursion guard
69 private static Boolean hasRunAfterUpdate = false;
70
71 public override void beforeInsert(List<SObject> newRecords) {
72 List<Opportunity> opps = (List<Opportunity>) newRecords;
73 OpportunityService.setDefaults(opps);
74 OpportunityService.validateAmounts(opps);
75 }
76
77 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 }
82
83 public override void afterUpdate(List<SObject> newRecords, Map<Id, SObject> oldMap) {
84 if (hasRunAfterUpdate) return; // Prevent recursion
85 hasRunAfterUpdate = true;
86
87 List<Opportunity> opps = (List<Opportunity>) newRecords;
88 Map<Id, Opportunity> oldOpps = (Map<Id, Opportunity>) oldMap;
89
90 // Only process opportunities that changed stage
91 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 }
97
98 if (!stageChanged.isEmpty()) {
99 OpportunityService.notifyStageChange(stageChanged);
100 OpportunityService.updateAccountRevenue(stageChanged);
101 }
102 }
103}
104
105// 5. The Trigger (thin — just 1 line of logic)
106// trigger OpportunityTrigger on Opportunity
107// (before insert, before update, after insert, after update, after delete) {
108// TriggerDispatcher.run(new OpportunityTriggerHandler());
109// }
110
111// 6. Service Class — Reusable business logic
112public class OpportunityService {
113
114 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 }
121
122 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 }
132
133 public static void validateStageTransitions(
134 List<Opportunity> newOpps, Map<Id, Opportunity> oldMap
135 ) {
136 // Prevent backward stage transitions
137 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' => 6
141 };
142
143 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);
147
148 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 }
156
157 public static void notifyStageChange(List<Opportunity> changedOpps) {
158 // Create tasks for sales managers
159 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 }
171
172 public static void updateAccountRevenue(List<Opportunity> opps) {
173 // Delegate to Queueable if complex
174 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:

  1. Implement the complete trigger framework (interface, dispatcher, base class) from scratch
  2. Create a concrete handler for the Account object that validates, sets defaults, and updates related records
  3. Add a bypass mechanism using Custom Metadata Type (Trigger_Setting__mdt) instead of static variables
  4. Write comprehensive test classes for your trigger handler covering all 7 trigger events
  5. Implement a recursion guard that prevents infinite trigger loops
  6. Build a trigger handler that tracks field changes: "which fields changed and what were the old values?"
  7. Create a trigger on Case that automatically escalates based on Priority and Age
  8. Test your framework with a Data Loader operation of 1000 records
  9. Add logging to your framework that records which handlers executed and how long each took
  10. 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.