The structure of triggers in Apex is quite clever. We all recognize their necessity and the best practice of separating logic from triggers for various reasons (discussed below). We’re also aware of the challenges in unit testing code directly within triggers.
Recently, I had to establish a trigger structure for a new client. Given my creativity and expertise as a developer, I naturally turned to Google first. Why reinvent the wheel when there are numerous existing structures available?
Utilization of Trigger Framework
A trigger manager system is a method for abstracting logic from your triggers and ensuring consistency across the platform. The system itself handles the heavy lifting by determining the type of trigger currently executing and executing the appropriate logic.
Here are some advantages of implementing a trigger framework:
Removing trigger logic from the trigger itself greatly simplifies unit testing and maintenance.
Standardizing triggers ensures that all triggers operate consistently.
Having a single trigger per object grants full control over the execution order.
It prevents trigger recursion.
It facilitates collaboration for large teams of engineers working across multiple objects within an organization. For instance, I recently collaborated with a company that had triggers on approximately fifty objects. The consistent execution of each trigger made it easy to modify existing triggers or add new ones.
Implementing a trigger system is particularly beneficial when multiple engineers are working within an organization. It allows the lead or architect to define how each trigger in the application should function. Utilizing a system enables the architect to make decisions such as “each trigger should execute a custom setting that allows us to disable the trigger.” Alternatively, the engineer may want to ensure that any validation occurring in a trigger is implemented through a method called “Approve()” on the trigger controller for consistency.
I’ve encountered organizations where different developers have their own ways of implementing triggers. This can make troubleshooting incredibly difficult, especially when a single trigger contains 1000 lines of code handling 20 different tasks.
Trigger Framework Specifications
Since I’ve criticized everyone else’s systems, here’s my attempt at something slightly less complicated.
The framework should be straightforward. Any developer should be able to understand how it functions without delving into a mass of standard code and comments. A single handler for each object to manage all events in bulkified form (eliminating the need for single record implementation). An interface for the trigger controllers to ensure consistency. Avoid complex trigger factory logic; instead, utilize basic dependency injection. Require the ability to deactivate triggers, which should be supported for each trigger. Initiating the framework on a new organization should be incredibly simple. Despite its lightweight nature, the framework must be scalable so it can be expanded upon as the organization’s requirements evolve. Developers only need to focus on writing their trigger controller class. They shouldn’t have to modify or even comprehend the underlying logic of the framework itself (for example, there’s no need to add another IF statement in a TriggerHandlerFactory class).
Trigger Interface
The interface specifies the methods that each trigger controller must implement, even if these methods are empty. By implementing these methods in this class, the TriggerFactory (discussed below) can ensure that the trigger controller has a method for each of these events:
- Before/After Insert
- Before/After Update
- Before/After Delete
- After Undelete
- IsDisabled
public interface ITriggerHandler{ void BeforeInsert(List<SObject> newRecord); void BeforeUpdate(Map<Id, SObject> newRecord, Map<Id, SObject> oldItems); void BeforeDelete(Map<Id, SObject> oldRecord); void AfterInsert(Map<Id, SObject> newRecord); void AfterUpdate(Map<Id, SObject> newRecord, Map<Id, SObject> oldRecord); void AfterDelete(Map<Id, SObject> oldRecord); void AfterUndelete(Map<Id, SObject> oldRecord); Boolean IsDisabled(); }
The Trigger Factory concept
public class TriggerDispatcher { public static void Run(ITriggerHandler handler) { if (handler.IsDisabled()) return; if (Trigger.IsBefore ){ if (Trigger.IsInsert) handler.BeforeInsert(trigger.new); if (Trigger.IsUpdate) handler.BeforeUpdate(trigger.newMap, trigger.oldMap); if (Trigger.IsDelete) handler.BeforeDelete(trigger.oldMap); } // After trigger logic if (Trigger.IsAfter) { if (Trigger.IsInsert) handler.AfterInsert(Trigger.newMap); if (Trigger.IsUpdate) handler.AfterUpdate(trigger.newMap, trigger.oldMap); if (trigger.IsDelete) handler.AfterDelete(trigger.oldMap); if (trigger.isUndelete) handler.AfterUndelete(trigger.oldMap); } } }
Trigger Controller
public class AccountTriggerHandler implements ITriggerHandler{ public static Boolean TriggerDisabled = false; public Boolean IsDisabled(){ if (TriggerSettings__c.AccountTriggerDisabled__c = true) return true; else return TriggerDisabled; } public void BeforeInsert(List<SObject> newItems){ for (Account acc : (List<Account>)newItems){ if (acc.Name.contains('test')) acc.Name.addError('You may not use the word "test" in the account name'); } } public void BeforeUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {} public void BeforeDelete(Map<Id, SObject> oldItems) {} public void AfterInsert(Map<Id, SObject> newItems) {} public void AfterUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {} public void AfterDelete(Map<Id, SObject> oldItems) {} public void AfterUndelete(Map<Id, SObject> oldItems) {} }
Apex Trigger
trigger AccountTrigger on Account (before insert, before update, before delete, after insert, after update, after delete, after undelete) { if(checkRecursive.runOnce()){ TriggerDispatcher.Run(new AccountTriggerHandler()); } }
Prevent Recursive Trigger
public Class checkRecursive { private static boolean run = 0; public static boolean runOnce() { if(run<2) { run=run=1; return true; } else { return run; } } }