I’ve read a lot of Apex Trigger code in my career. While I was consulting, I was able to see the good, the bad, and the ugly when it came to Trigger implementations. For the triggers that took a wrong turn at some point, the story was almost always the same.
The initial trigger was small, and served a single purpose. Over time, as new business requirements emerged, that once tiny trigger started growing and growing into a monstrosity. New functionality was routinely tacked on and technical debt began to accumulate. Now the logic was hard, if not impossible to follow. It was also becoming increasingly difficult to maintain because even the slightest change might mean rewriting the whole trigger from scratch. The tests were unreliable. Trigger logic was executing out of order and governor limits were getting hit left and right. I took a good look at not only the bad triggers that I saw, but the good ones as well. I started coming up with some best-practices that would help developers avoid some of these pitfalls. So before we get into Trigger Frameworks, which is the subject of this article, let’s first talk about some of the best practices developers should be aware of when it comes to trigger writing.
Trigger Best Practices
One Trigger Per Object
A single Apex Trigger is all you need for one particular object. If you develop multiple Triggers for a single object, you have no way of controlling the order of execution if those Triggers can run in the same contexts. Many times, the order of execution doesn’t matter but when it does matter, it’s nearly impossible to maintain proper flow control. A single Trigger can handle all possible combinations of Trigger contexts which are:
- before insert
- after insert
- before update
- after update
- before delete
- after delete
- after undelete
So as a best practice, create one Trigger per object and let it handle all of the contexts that you need. Here is an example of a Trigger that implements all possible contexts:
1 | trigger OpportunityTrigger on Opportunity ( |
2 | before insert , before update , before delete, |
3 | after insert , after update , after delete, after undelete) { |
Logic-less Triggers
Another widely-recognized best practice is to make your Triggers logic-less. That means, the role of the Trigger is just to delegate the logic responsibilities to some other handler class. There are many reasons to do this. For one, testing a Trigger is difficult if all of the application logic is in the trigger itself. If you write methods in your Triggers, those can’t be exposed for test purposes. You also can’t expose logic to be re-used anywhere else in your org. Good old OO principles tell us that this is a bad practice. And to top it all off, cramming all of your logic into a Trigger is going to make for a mess one day. To remedy this scenario, just create a handler class and let your Trigger delegate to it. Here is an example:
trigger OpportunityTrigger on Opportunity (after insert) {
1 | OpportunityTriggerHandler.handleAfterInsert(Trigger. new ); |
04 | public class OpportunityTriggerHandler { |
06 | public static void handleAfterInsert( List opps) { |
Context-Specific Handler Methods
One best-practice that I have picked up is to create context-specific handler methods in my Trigger handlers. In the above example, you’ll see that I’ve created a specific handler method just for after insert. If I were to implement new logic that ran on after update, I’d simply add a new handler method for it. Again, this handler method would be in the handler class, and not the Trigger. In this case, I might add some very light routing logic into the Trigger itself just so that the correct handler method is invoked:
1 | trigger OpportunityTrigger on Opportunity (after insert , after update ) { |
3 | if (Trigger.isAfter && Trigger.isInsert) { |
4 | OpportunityTriggerHandler.handleAfterInsert(Trigger. new ); |
5 | } else if (Trigger.isAfter && Trigger.isUpdate) { |
6 | OpportunityTriggerHandler.handleAfterInsert(Trigger. new , Trigger.old); |
Why Use a Framework?
So now that we have some best-practices out of the way, we can talk about frameworks and why we would want to use them. At this point you might be asking, “if I follow best-practices, do I really need a framework”? The short answer is no, you don’t need always need a framework to write your Triggers. A framework may, however, greatly simplify your development efforts when your code base gets large. In a nutshell, your framework should have the following goals:
- Help you to conform to best practices
- Make implementing new logic and new context handlers very easy
- Simplify testing and maintenance of your application logic
- Enforces consistent implementation of Trigger logic
- Implement tools, utilities, and abstractions to make your handler logic as lightweight as possible
What Can Frameworks Do?
In addition to making coding cleaner and more consistent, your framework can solve a bunch of problems for you.
Routing Abstractions
In the example code for the “Context-Specific Handler Methods”, you probably noticed that I still needed to implement routing logic in the trigger. This logic looked at the context that the trigger was running in and dispatched to the correct handler method. This is something that an underlying Trigger framework could easily handle for you.
Recursion Detection and Prevention
A Trigger framework can also act like a watchdog over all Trigger executions. A common mistake that developers have is building their Trigger logic so that Triggers execute recursively. When this happens, unexpected things may occur and even governor limits can be hit. Your Trigger framework can detect this and figure out how to handle the situation properly.
Centralize Enable/Disable of Triggers
As someone who knows the pain of deploying a Trigger to production that contains bugs, I can tell you that giving yourself the ability to easily disable a trigger that isn’t functioning properly is a huge win. Your Trigger framework can easily be wired up to a Custom Setting in your org to give you on/off control of your triggers. This is a great thing to have when you need to buy some time to patch broken code and don’t want your users to be interrupted by Apex exceptions on their pages!
Example Framework
Ok, now let’s look at a framework in action. For this exercise, I’m going to use my own Trigger framework that I developed and [
open-sourced on Github. Let me first start by showing the basic implementation for an Opportunity Trigger.
Basic Implementation
Here is what you need to implement for the trigger. Notice how the body is a one-liner despite handling 4 separate contexts!
01 | trigger OpportunityTrigger on Opportunity (after insert , after update , after delete, after undelete) { |
02 | new OpportunityTriggerHandler().run(); |
04 | And here is the handler class we will create: |
06 | public class OpportunityTriggerHandler extends TriggerHandler { |
08 | public OpportunityTriggerHandler() {} |
Adding Logic
Now we haven’t implemented any logic yet. So how would we do that? If you notice, the OpportunityTriggerHandler inherits from a base TriggerHandler class. The TriggerHandler class is where all of the magic is. That class already defines overridable context handler methods that are automatically called when that context is detected. So let’s say we get a requirement to update the Amount field to zero when an opportunity is marked Closed/Lost. This would make a lot of sense in a before update Trigger. To get started, all we need to do is override the before update method and implement our logic. It will automatically be called by the TriggerHandler class.
01 | public class OpportunityTriggerHandler extends TriggerHandler { |
03 | public OpportunityTriggerHandler() {} |
13 | private void setLostOppsToZero( List ) { |
14 | for (Opportunity o : ( List <Opportunity > ) Trigger. new ) { |
15 | if (o.StageName = = 'Closed Lost' && o.Amount > 0 ) { |
Notice how we didn’t need to make any changes to the Trigger itself and all we needed to do was to override the beforeUpdate() method in the handler class? The only other thing that we needed to do was cast Trigger.new to a List<Opportunity>. That’s because there is a little magic that happens within an actual Trigger that handles the casting for you. Outside of the Trigger itself, statics like Trigger.new and Trigger.newMap always contain raw sObjects or Maps of sObjects respectively.
What’s great now is that any time there is a new requirement for Trigger logic that requires a new context handler, we just need to override that handler method and implement our logic:
01 | public class OpportunityTriggerHandler extends TriggerHandler { |
03 | public OpportunityTriggerHandler() {} |
As you can see, the resulting Trigger handler class is easy to understand and easy to update when your requirements change.
How does it all work?
So far we’ve shown how to implement this framework, but you might be asking how it actually works? It’s really quite simple. As you saw in the previous steps, we create Trigger handler classes that inherit from a a base class provided by the framework called TriggerHandler. This trigger handler has several roles:
- Define overridable methods for each context
- Provide routing logic to call the correct context method
- Supervise and manage Trigger execution
The run() method
Let’s look at the
source code for the TriggerHandler class. The
run() method is what is called by the Trigger when the handler has been initialized and it’s time to run our Trigger logic. This logic simply runs any supervisory logic first, then checks the current context for the trigger, then calls it’s own handler method for it. Here is what that block looks like:
03 | if (!validateRun()) return ; |
08 | if (Trigger.isBefore && Trigger.isInsert) { |
10 | } else if (Trigger.isBefore && Trigger.isUpdate) { |
12 | } else if (Trigger.isBefore && Trigger.isDelete) { |
14 | } else if (Trigger.isAfter && Trigger.isInsert) { |
16 | } else if (Trigger.isAfter && Trigger.isUpdate) { |
18 | } else if (Trigger.isAfter && Trigger.isDelete) { |
20 | } else if (Trigger.isAfter && Trigger.isUndelete) { |
The handler methods like afterInsert() are defined logic-less and meant to be overridden. If they aren’t overridden, nothing really happens. Here’s what one of those methods look like in the TriggerHandler class:
1 | protected virtual void afterInsert(){} |
Supervisor Logic
In addition to serving as a base handler for our Trigger handlers, our base TriggerHandler class can also manage the behavior of Trigger executions across all Triggers. You probably noticed that a few methods were called at the beginning of the run method before we got the context routing block. These parts of the run method are what I like to call “supervisor logic”.
Recursion Protection
In my framework, I provide a utility that can prevent a single TriggerHandler from firing recursively. The implementation is pretty simple and allows you to set the number of executions per Trigger:
01 | public class OpportunityTriggerHandler extends TriggerHandler { |
03 | public OpportunityTriggerHandler() { |
04 | this .setMaxLoopCount( 1 ); |
07 | public override void afterUpdate() { |
08 | List opps = [ SELECT Id FROM Opportunity WHERE Id IN :Trigger.newMap.keySet()]; |
A Bypass API?
One example that I added is a Trigger bypass API. To be totally honest, I’m not even sure if programmatically bypassing Triggers is a good idea. I just added it into my framework to show the power of a central control for Trigger execution.
So here’s the scenario. Let’s say that you have one Trigger that invokes another trigger when inserting records. An example might be some Trigger logic on Opportunity insertions that creates a Case record for new Opportunities. If that Case object has a Trigger on insertions, both the Opportunity Trigger and Case Trigger will have run.
The bypass API allows you to tell the Case Trigger to not run at all. And, to take it even further, you could re-enable the Case Trigger at some other point in the logic. Here is an example from an implemented Opportunity Trigger handler:
01 | public class OpportunityTriggerHandler extends TriggerHandler { |
03 | public override void afterUpdate() { |
04 | List opps = [ SELECT Id, AccountId FROM Opportunity WHERE Id IN :Trigger.newMap.keySet()]; |
06 | Account acc = [ SELECT Id, Name FROM Account WHERE Id = :opps. get ( 0 ).AccountId]; |
09 | c.Subject = 'My Bypassed Case' ; |
11 | TriggerHandler.bypass( 'CaseTriggerHandler' ); |
15 | TriggerHandler.clearBypass( 'CaseTriggerHandler' ); |
17 | c.Subject = 'No More Bypass' ; |
Again, I don’t know how useful this would be in the real world, but it’s a good example of centralizing control in a Trigger Framework.
On/Off Switch
I had a great idea submitted via Pull Request by
James Loghry. He submitted a change that linked a Custom Settings object to the TriggerHandler so that an administrator could easily enable or disable a Trigger from Salesforce Custom Settings. This is really handy when a Trigger is causing production issues and you don’t have time to re-deploy. This could be extended into a Visualforce interface that you could provide to your admins.
Taking it From Here
I hope showing the examples and walking through the code has shown you some of the things that are possible when you implement your Triggers in a framework. If you like my framework, I highly encourage you to fork it and modify it to fit your needs. I’m also open to Pull Requests. You can also take a fresh approach and build your own.
In addition, I have found some other frameworks out in the wild that are worth checking out.
-
- Hari Krishnan’s Apex Trigger Architecture Framework
- Tony Scott’s Trigger Pattern Recipe
No comments:
Post a Comment