The trigger framework pattern: trigger does nothing but delegate; logic lives in handler classes.
The trigger (one per object):
apex trigger AccountTrigger on Account (before insert, before update, after insert, after update, after delete) { new AccountTriggerHandler().run(); }
The handler base class:
apex 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:
`apex 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).
