A trigger framework is the difference between Apex automation that is maintainable, testable and extensible versus a collection of ad-hoc trigger files that become increasingly dangerous to modify as an org grows.
Why frameworks matter
Without a framework, triggers accumulate organically. Developer A writes one trigger for field defaulting. Developer B writes another for notifications. Developer C writes a third for integration callouts. All three are on the same object. Salesforce executes them in an unpredictable order. One trigger’s DML causes another to fire. A bug in one trigger affects all three. Testing any of them requires testing all of them.
A trigger framework imposes structure that prevents this entropy.
The minimal framework pattern
The minimal implementation has three components: one trigger per object (the dispatcher), a handler class (the logic), and a recursion guard.
// AccountTrigger.trigger — the dispatcher (nothing else here)
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
AccountTriggerHandler handler = new AccountTriggerHandler();
handler.dispatch();
}
```apex
// AccountTriggerHandler.cls — all logic lives here
public with sharing class AccountTriggerHandler {
// Recursion guard
private static Boolean isRunning = false;
public void dispatch() {
if (isRunning) return;
isRunning = true;
try {
if (Trigger.isBefore) {
if (Trigger.isInsert) onBeforeInsert(Trigger.new);
if (Trigger.isUpdate) onBeforeUpdate(Trigger.new, Trigger.oldMap);
}
if (Trigger.isAfter) {
if (Trigger.isInsert) onAfterInsert(Trigger.new, Trigger.newMap);
if (Trigger.isUpdate) onAfterUpdate(Trigger.new, Trigger.newMap, Trigger.oldMap);
}
} finally {
isRunning = false;
}
}
private void onBeforeInsert(List<Account> newAccounts) {
// Default field values, set computed fields
AccountService.defaultFields(newAccounts);
}
private void onAfterInsert(List<Account> newAccounts, Map<Id, Account> newMap) {
// Create related records, send notifications
AccountService.createDefaultContacts(newAccounts);
}
private void onBeforeUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
AccountService.handleStatusChange(newAccounts, oldMap);
}
private void onAfterUpdate(
List<Account> newAccounts,
Map<Id, Account> newMap,
Map<Id, Account> oldMap
) {
AccountService.syncRelatedOpportunities(newAccounts, oldMap);
}
## The Service class pattern
The handler dispatches to service classes that each own a specific domain
of business logic. This keeps the handler as a routing layer and the
service classes as the testable units of logic.
```apex
public with sharing class AccountService {
public static void defaultFields(List<Account> accounts) {
for (Account acc : accounts) {
if (acc.Rating == null) acc.Rating = 'Warm';
}
}
public static void handleStatusChange(
List<Account> accounts,
Map<Id, Account> oldMap
) {
List<Account> changed = new List<Account>();
for (Account acc : accounts) {
if (acc.AccountSource != oldMap.get(acc.Id).AccountSource) {
changed.add(acc);
}
}
if (!changed.isEmpty()) {
// Handle the subset of changed records
}
}
}
## The bypass mechanism
Every framework should include a way to disable trigger execution for
specific contexts — data migrations, bulk loads, or emergency maintenance.
```apex
public with sharing class TriggerBypass {
public static Boolean isBypassed(String handlerName) {
// Query Custom Metadata: Trigger_Bypass__mdt
// Return true if bypass is active for this handler
return Trigger_Bypass__mdt.getInstance(handlerName)?.Active__c == true;
}
}
This allows an admin to activate a Custom Metadata bypass record without
deploying code — zero-downtime trigger disabling. Test your knowledge — Apex
10 questions · Basic to Advanced