.trigger files and into modular Apex classes. By routing database events through a single handler and dispatcher per object, it ensures predictable execution order, prevents infinite recursion loops, and makes code testable and reusable.
Writing business logic directly inside trigger definitions quickly creates monolithic, unmanageable code. When multiple triggers execute on the same sObject, Salesforce does not guarantee the order in which they fire. An enterprise Apex Trigger Framework enforces the foundational "One Trigger Per Object" rule, separating event orchestration from business logic and ensuring complete control over execution flow.
1. Why Every Salesforce Org Needs a Trigger Framework
Without a structured framework, enterprise codebases suffer from severe maintainability and performance bottlenecks:
- Unpredictable Execution Order: Multiple independent triggers on the same object fire in non-deterministic order, causing race conditions and difficult-to-reproduce bugs.
- Uncontrolled Recursion: Field updates in after-triggers can trigger the same event repeatedly until the transaction hits governor limits.
- Untestable Logic: Business logic embedded directly in triggers cannot be unit-tested in isolation without performing full database DML inserts and updates.
- Code Duplication: Identical validation and data enrichment operations get rewritten across separate classes instead of being shared.
- Golden Rule: Exactly one trigger per sObject (e.g., only
AccountTrigger.trigger). - Core Layers: Trigger file (listener) → Trigger Handler (orchestrator) → Service Classes (domain logic).
- Context Management: Ingests
Trigger.operationType,Trigger.new, andTrigger.oldMap. - Recursion Guard: Centralized bypass and static execution flags to stop infinite trigger loops.
2. Core Architectural Components
A production-ready trigger architecture consists of three distinct layers:
- 1. The Trigger Definition: A lightweight listener containing no business logic. It delegates immediately to the handler's execution method.
- 2. The Virtual Trigger Handler Base: An abstract or virtual class providing default lifecycle methods (
beforeInsert,afterUpdate, etc.) that specific object handlers override. - 3. Specific Object Handlers & Service Layer: Concrete classes that implement object-specific rules (e.g.,
AccountTriggerHandler) and call specialized service classes for heavy calculations.
3. Step-by-Step Implementation: Building a Clean Trigger Framework
This base class acts as the framework engine, inspecting
Trigger.operationType and routing to the right lifecycle method while managing bypass controls.
public virtual class TriggerHandlerRoot {
// Tracks handler bypass status for data loads and unit tests
private static Set<String> bypassedHandlers = new Set<String>();
// Context-specific overridable methods
protected virtual void beforeInsert() {}
protected virtual void beforeUpdate() {}
protected virtual void beforeDelete() {}
protected virtual void afterInsert() {}
protected virtual void afterUpdate() {}
protected virtual void afterDelete() {}
protected virtual void afterUndelete() {}
// Master dispatcher method
public void run() {
if (!Trigger.isExecuting) {
throw new TriggerException('Handler invoked outside of trigger context.');
}
String handlerName = String.valueOf(this).substring(0, String.valueOf(this).indexOf(':'));
if (bypassedHandlers.contains(handlerName)) {
return;
}
switch on Trigger.operationType {
when BEFORE_INSERT { this.beforeInsert(); }
when BEFORE_UPDATE { this.beforeUpdate(); }
when BEFORE_DELETE { this.beforeDelete(); }
when AFTER_INSERT { this.afterInsert(); }
when AFTER_UPDATE { this.afterUpdate(); }
when AFTER_DELETE { this.afterDelete(); }
when AFTER_UNDELETE{ this.afterUndelete(); }
}
}
public static void bypass(String handlerName) {
bypassedHandlers.add(handlerName);
}
public static void clearBypass(String handlerName) {
bypassedHandlers.remove(handlerName);
}
public class TriggerException extends Exception {}
}
Extend the base class and override only the context methods needed for that sObject.
public with sharing class AccountTriggerHandler extends TriggerHandlerRoot {
private List<Account> newRecordList;
private Map<Id, Account> newRecordMap;
private Map<Id, Account> oldRecordMap;
public AccountTriggerHandler() {
this.newRecordList = (List<Account>) Trigger.new;
this.newRecordMap = (Map<Id, Account>) Trigger.newMap;
this.oldRecordMap = (Map<Id, Account>) Trigger.oldMap;
}
protected override void beforeInsert() {
AccountService.setDefaultAccountFields(this.newRecordList);
}
protected override void beforeUpdate() {
AccountService.validateRevenueChanges(this.newRecordList, this.oldRecordMap);
}
protected override void afterUpdate() {
AccountService.syncChildContacts(this.newRecordMap, this.oldRecordMap);
}
}
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
new AccountTriggerHandler().run();
}
4. Managing Recursion, Bulkification & Unit Testing
Trigger frameworks must handle bulk database transactions and maintain isolation during unit testing:
- Bulkification by Default: Ensure handler methods process entire collections (
List<sObject>) using sets and maps, keeping all SOQL queries and DML statements strictly outside loops. - Static Run-Once Guards: Use a static
Set<Id> processedIdsinside your service classes to ensure child record synchronization executes only once per transaction. - Bypass Capability for Tests: Use
TriggerHandlerRoot.bypass('AccountTriggerHandler')during data setup in unit tests to quickly create records without burning CPU time on trigger logic.
5. Common Traps & Architect Best Practices
Having more than one trigger per object leads to unpredictable order of execution and broken integrations. Additionally, embedding SOQL queries and DML statements directly in handler switch statements creates bloated classes. Always delegate data queries and DML operations to dedicated Service Layer classes.
- Enforce User Mode in Handlers: Always respect sharing and Field-Level Security by appending
WITH USER_MODEon queries and usingas useron DML statements. - Feature Flag Integration: Integrate Custom Metadata Types or Custom Permissions into the handler to allow administrators to toggle specific triggers on or off dynamically in production without deploying code.
- Test Bulk Scenarios (200+ Records): Write unit tests verifying that handlers execute cleanly across 200 records without hitting governor limits.
Summary
Implementing an Apex Trigger Framework standardizes your development lifecycle and prevents technical debt in enterprise Salesforce applications. By decoupling the trigger listener from handler execution, enforcing single-trigger architectures, and incorporating bypass controls, engineering teams can build scalable, maintainable, and high-performance automation workflows.