As Salesforce organizations grow, triggers frequently become messy, unmaintainable "spaghetti code." When multiple developers add logic directly to an object's triggers without a unified pattern, it leads to unpredictable execution order, severe recursion bugs, and unexpected governor limit breaches.
In this guide, we will look at how to build a lightweight, object-oriented Trigger Handler framework that enforces the Single Responsibility Principle (SRP), controls execution order cleanly, and provides built-in recursion and bypass management for enterprise-scale orgs.
- The Problem with Unstructured Triggers In a standard unstructured setup, you might see multiple triggers on the same object (e.g., AccountTrigger1, AccountTrigger2) or a single monolithic trigger crammed with business logic, queries, and DML statements.
This causes three major bottlenecks:
1.Unpredictable Execution Order
Salesforce does not guarantee the order of execution between multiple triggers on the same sObject.
Recursion Nightmares: Updates triggered by downstream automation can accidentally re-fire the same trigger infinitely.
Difficult Unit Testing: Hardcoding business logic directly in triggers makes it nearly impossible to isolate code for testing without hitting unnecessary DML limits.
- The Architectural Solution To solve this, we can implement a Base Trigger Handler Pattern.
The One-Trigger-Per-Object Rule: Every sObject has exactly one trigger.
Virtual Base Class: A reusable base class (TriggerHandler) manages trigger context (Trigger.isBefore, Trigger.isAfter, etc.), routing execution to specific lifecycle methods.
Domain Handlers: Individual handler classes (AccountTriggerHandler) inherit from the base class and override only the methods they need (e.g., beforeInsert(), afterUpdate()).
- Production-Ready Code Implementation A. The Base Trigger Handler (TriggerHandler.cls) This abstract/virtual base class routes trigger events and handles global bypass logic.
Apex
public virtual class TriggerHandler {
private static Set bypassedHandlers = new Set();
// Virtual lifecycle methods to be overridden in domain handlers
@TestVisible protected virtual void beforeInsert() {}
@TestVisible protected virtual void afterInsert() {}
@TestVisible protected virtual void beforeUpdate() {}
@TestVisible protected virtual void afterUpdate() {}
@TestVisible protected virtual void beforeDelete() {}
@TestVisible protected virtual void afterDelete() {}
@TestVisible protected virtual void afterUnDelete() {}
public void run() {
if (!validateRun()) return;
switch on Trigger.operationType {
when BEFORE_INSERT { this.beforeInsert(); }
when AFTER_INSERT { this.afterInsert(); }
when BEFORE_UPDATE { this.beforeUpdate(); }
when AFTER_UPDATE { this.afterUpdate(); }
when BEFORE_DELETE { this.beforeDelete(); }
when AFTER_DELETE { this.afterDelete(); }
when AFTER_UNDELETE{ this.afterUnDelete(); }
}
}
private Boolean validateRun() {
String handlerName = String.valueOf(this).split(':')[0];
return !bypassedHandlers.contains(handlerName) && Trigger.isExecuting;
}
public static void bypass(String handlerName) {
bypassedHandlers.add(handlerName);
}
public static void clearBypass(String handlerName) {
bypassedHandlers.remove(handlerName);
}
@TestVisible
public static void clearAllBypasses() {
bypassedHandlers.clear();
}
}
B. Domain Handler Implementation (AccountTriggerHandler.cls)
Here is how clean your object-specific logic becomes. We can now notice how triggers no longer contain raw business logic—they delegate work to service or handler methods.
Apex
public with sharing class AccountTriggerHandler extends TriggerHandler {
protected override void beforeInsert() {
// Delegate domain logic to service classes
AccountService.normalizeBillingAddresses((List) Trigger.new);
}
protected override void afterUpdate() {
AccountService.syncRelatedContacts(
(List) Trigger.new,
(Map) Trigger.oldMap);
}
}
C. The Single Dispatcher Trigger (AccountTrigger.trigger)
Your actual database trigger file is now reduced to 3 clean lines of code.
Apex
trigger AccountTrigger on Account (before insert, after insert, before update, after update) {
new AccountTriggerHandler().run();
}
- Key Enterprise Benefits Easy Trigger Bypassing: When running batch jobs, data migrations, or complex integrations where triggers need to be skipped, you can easily turn them off dynamically:
Apex
TriggerHandler.bypass('AccountTriggerHandler');
// Perform bulk data operation...
TriggerHandler.clearBypass('AccountTriggerHandler');
Clean Separation of Concerns: Triggers only listen; handlers route; service classes execute business logic.
Predictable Debugging: Stack traces become significantly easier to read because every execution flows through a standardized dispatch method.
Conclusion---
Adopting a modular trigger framework takes very little setup time, but it pays massive dividends as your Salesforce org scales. It keeps your code base clean, testable, and friendly for fellow developers collaborating on the same environment.
Have you implemented a custom trigger framework in your org, or do you use a package framework like fflib? Let's discuss in the comments below!
Top comments (0)