The trigger framework pattern: trigger does nothing but delegate; logic lives in handler classes.
The trigger (one per object):
trigger AccountTrigger on Account (before insert, before update, after insert, after update, after delete) {
new AccountTriggerHandler().run();
}The handler base class:
public virtual class TriggerHandler {
public void run() {
if (Trigger.isBefore) {
if (Trigger.isInsert) beforeInsert();
if (Trigger.isUpdate) beforeUpdate();
} else { // isAfter
if (Trigger.isInsert) afterInsert();
if (Trigger.isUpdate) afterUpdate();
if (Trigger.isDelete) afterDelete();
}
}
public virtual void beforeInsert() {}
public virtual void beforeUpdate() {}
public virtual void afterInsert() {}
public virtual void afterUpdate() {}
public virtual void afterDelete() {}
}The Account-specific handler:
public class AccountTriggerHandler extends TriggerHandler {
public override void afterUpdate() {
// Bulkified pattern: collect Ids, query once, process in memory, DML once.
Set<Id> changedAccIds = new Set<Id>();
Map<Id, Account> oldMap = (Map<Id, Account>) Trigger.oldMap;
for (Account a : (List<Account>) Trigger.new) {
if (a.Phone != oldMap.get(a.Id).Phone) {
changedAccIds.add(a.Id);
}
}
if (changedAccIds.isEmpty()) return;
List<Contact> toUpdate = new List<Contact>();
Map<Id, Account> newMap = (Map<Id, Account>) Trigger.newMap;
for (Contact c : [SELECT Id, AccountId, Phone FROM Contact WHERE AccountId IN :changedAccIds]) {
c.Phone = newMap.get(c.AccountId).Phone;
toUpdate.add(c);
}
if (!toUpdate.isEmpty()) update toUpdate;
}
}Why this is good:
- One trigger per object — clear orchestration point.
- Handler is testable — mock the trigger context in tests; no need to fire actual DML.
- Bulkified — Sets, Maps, queries outside loops, single DML.
- Conditional work — early return when nothing changed.
- Type casts isolated — handler does the cast once.
Add as you grow:
- A
TriggerHandlerFactorythat maps sObject name to handler class for dynamic dispatch. - A static recursion guard inside each handler to prevent re-entry.
- Per-record action queues for cross-trigger collaboration (e.g., a handler updates state used by another handler in the same transaction).