The Trigger Handler Pattern
A trigger handler pattern separates the trigger itself — a thin, declarative entry point — from the actual business logic, which lives in a dedicated handler class instantiated and invoked by the trigger. The trigger body becomes as simple as calling a single handler method, while the handler class contains methods like beforeInsert(), afterUpdate(), and beforeDelete() that map directly to the trigger context Salesforce fires. This separation exists because Apex allows multiple triggers per object but does not guarantee their execution order, so consolidating all logic for an object into one trigger delegating to one handler eliminates that non-determinism entirely.
Cricket analogy: A captain who calls every fielding position and bowling change from a single, coordinated game plan avoids the chaos of players independently deciding strategy — a single trigger delegating to one handler avoids the chaos of multiple uncoordinated triggers firing in unpredictable order.
Designing the Handler Class
A well-designed handler class implements distinct methods for each trigger context — beforeInsert, beforeUpdate, afterInsert, afterUpdate, beforeDelete, afterDelete, afterUndelete — often via a shared interface, so the trigger's dispatcher calls only the method relevant to the current Trigger.operationType. Before-context methods are used for field validation and default-value assignment on the in-memory records (since Trigger.new is mutable before insert/update), while after-context methods are used for operations touching related records via DML, since Trigger.new is read-only in after context and any downstream update requires a fresh SOQL/DML pass.
Cricket analogy: A team's pre-match warm-up focuses on fixing technique and stance before the ball is bowled, while the post-innings debrief reviews what happened and plans follow-up actions — beforeInsert validates/defaults fields in-memory while afterUpdate triggers downstream actions like tasks.
public class AccountTriggerHandler implements TriggerHandlerInterface {
public void beforeInsert(List<Account> newAccounts) {
for (Account acc : newAccounts) {
if (String.isBlank(acc.Rating)) {
acc.Rating = 'Warm';
}
}
}
public void afterUpdate(Map<Id, Account> newMap, Map<Id, Account> oldMap) {
List<Task> followUps = new List<Task>();
for (Account acc : newMap.values()) {
Account old = oldMap.get(acc.Id);
if (acc.Rating == 'Hot' && old.Rating != 'Hot') {
followUps.add(new Task(
WhatId = acc.Id,
Subject = 'Follow up: account upgraded to Hot',
ActivityDate = Date.today().addDays(1)
));
}
}
if (!followUps.isEmpty()) {
insert followUps;
}
}
}Preventing Recursion
Because an afterUpdate method that itself performs update on the same object's records will re-fire the trigger, a robust handler guards against this with a static Boolean flag scoped to the transaction, checked at the very start of the handler's dispatch method and set to true before the sensitive logic runs; since static variables persist for the lifetime of a single transaction, this reliably blocks only the unwanted re-entrant call without affecting legitimate future updates. A more scalable variant uses a static Set<Id> of already-processed record Ids rather than a single flag, which allows the trigger to correctly process new records introduced later in the same transaction while still skipping ones already handled.
Cricket analogy: A DRS review system that flags 'this delivery has already been reviewed' prevents a team from requesting a second review on the same ball — a static flag prevents a trigger from re-processing the same records within one transaction.
Forgetting a recursion guard on an afterUpdate handler that itself performs a DML update on the same object is one of the fastest ways to hit CPU time limits or create duplicate related records (e.g., doubled Tasks or emails) in production.
Context Variables and Maps
Trigger.new, Trigger.old, Trigger.newMap, and Trigger.oldMap are the core context variables a handler consumes — newMap and oldMap are only populated in update, delete, and undelete contexts (never insert, since there's no 'old' state, and never delete for newMap since there's no 'new' state), and referencing the wrong one for the current context throws a runtime error. Comparing old and new field values field-by-field inside a loop is the standard way to detect exactly what changed and avoid firing downstream logic (like sending an email) when the update didn't actually touch the relevant field.
Cricket analogy: Comparing a batsman's stats before and after this innings tells the analyst exactly what changed — comparing Trigger.oldMap and Trigger.newMap field-by-field tells the handler exactly which fields actually changed.
- Use one trigger per object that delegates immediately to a handler class implementing context-specific methods (beforeInsert, afterUpdate, etc.).
- Before-context methods mutate Trigger.new directly for validation and defaults; after-context methods handle related-record DML since Trigger.new is read-only there.
- Guard against recursive trigger execution with a static Boolean flag or a static Set<Id> of already-processed record Ids.
- Trigger.newMap and Trigger.oldMap are only available in update/delete/undelete contexts, not insert (no oldMap) or delete (no newMap).
- Compare old and new field values explicitly to detect real changes and avoid firing downstream logic unnecessarily.
- Bulkify all handler methods — they receive Lists and Maps of potentially 200 records, never a single record.
- A shared interface keeps handler classes consistent and makes the dispatcher logic reusable across objects.
Practice what you learned
1. In which trigger context is Trigger.new mutable, allowing field defaults to be set directly?
2. Why is Trigger.oldMap unavailable during an insert operation?
3. What is the purpose of a static Boolean flag in a trigger handler?
4. Why should trigger logic be delegated to a handler class rather than written inline in the trigger?
5. What is the recommended way to detect whether a specific field actually changed during an update?
Was this page helpful?
You May Also Like
Apex Best Practices
The core disciplines — bulkification, selective SOQL, and layered architecture — that keep Apex code safe at production scale.
Common Apex Pitfalls
The recurring bugs — unbulkified loops, hardcoded Ids, recursive triggers, and swallowed exceptions — that cause most Apex production incidents.
Apex Quick Reference
A fast lookup for Apex collection syntax, SOQL/DML statement forms, and the exact governor limit numbers that matter day-to-day.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics