Apex Triggers & the Handler Pattern
Trigger events, context variables, before vs after, one-trigger-per-object, recursion control and addError.
Developer 3 min readTriggersTrigger.newHandler patternRecursion
Syntax and events
apex
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
AccountTriggerHandler.run();
}
Before vs after
| Before trigger | After trigger | |
|---|---|---|
| Use for | Validating / changing the same records | Work needing the record Id or touching other records |
Change fields on Trigger.new | ✅ no DML needed | ❌ read-only (FinalException) |
Id available on insert | ❌ | ✅ |
| Typical example | Default a field, addError() | Create child records, roll-ups, callouts (via async) |
Context variables
| Variable | Available in |
|---|---|
Trigger.new | insert, update, undelete |
Trigger.old | update, delete |
Trigger.newMap | before update, after insert/update/undelete |
Trigger.oldMap | update, delete |
Trigger.isBefore / isAfter / isInsert / isUpdate / isDelete / isUndelete | always |
Trigger.operationType | TriggerOperation.BEFORE_INSERT, … |
Trigger.size | number of records in this chunk (≤ 200) |
Detecting a change (update triggers)
apex
for (Opportunity opp : Trigger.new) {
Opportunity old = Trigger.oldMap.get(opp.Id);
if (opp.StageName != old.StageName && opp.StageName == 'Closed Won') {
// react only to the transition
}
}
Blocking records with addError
apex
trigger AccountTrigger on Account (before delete) {
Set<Id> withOpenOpps = new Set<Id>();
for (Opportunity o : [SELECT AccountId FROM Opportunity
WHERE IsClosed = false AND AccountId IN :Trigger.oldMap.keySet()]) {
withOpenOpps.add(o.AccountId);
}
for (Account acc : Trigger.old) {
if (withOpenOpps.contains(acc.Id)) {
acc.addError('Cannot delete an account with open opportunities');
}
}
}
addError rejects just that record (or the whole DML when allOrNone = true) and shows the message in the UI.
The handler pattern (one trigger per object)
apex
trigger OpportunityTrigger on Opportunity (before insert, before update, after update) {
new OpportunityTriggerHandler().run();
}
public with sharing class OpportunityTriggerHandler {
public void run() {
switch on Trigger.operationType {
when BEFORE_INSERT { setDefaults((List<Opportunity>) Trigger.new); }
when BEFORE_UPDATE { setDefaults((List<Opportunity>) Trigger.new); }
when AFTER_UPDATE { markCustomers((List<Opportunity>) Trigger.new,
(Map<Id, Opportunity>) Trigger.oldMap); }
}
}
private void setDefaults(List<Opportunity> opps) { /* … */ }
private void markCustomers(List<Opportunity> opps, Map<Id, Opportunity> oldMap) { /* … */ }
}
Why: predictable order (multiple triggers on one object run in no guaranteed order), testable logic, easy to disable.
Recursion control
apex
public class TriggerGuard {
private static Set<Id> processed = new Set<Id>();
public static List<SObject> firstTime(List<SObject> recs) {
List<SObject> out = new List<SObject>();
for (SObject r : recs) if (processed.add(r.Id)) out.add(r);
return out;
}
}
A plain static Boolean flag also works but skips records in later 200-record chunks of the same transaction — a Set of processed Ids is safer.
Interview questions
- When do you use a before trigger instead of after?
- Why is
Trigger.newMapnull in before insert? → Records have no Id yet. - How do you prevent a trigger from running twice? → Static Set/flag (recursion guard).
- Order of multiple triggers on the same object? → Not guaranteed — use one trigger per object.