Read more at, or return to the homepage

Lightweight Trigger Handler

Tap into the power of the Trigger Handler pattern in Salesforce with this extremely lightweight Trigger Handler framework. I was doing some research for an upcoming post (on how to write performant tests, and why that matters) when I name-dropped probably the single most used pattern on the SFDC platform — the concept of the single "handler" class per SObject trigger: the Trigger Handler pattern.

A number of prominent SFDC personalities — Kevin O'Hara, Dan Appleman, to name two — have championed this pattern over the years. Despite this, it lacks support from the official Salesforce documentation on Triggers ... although I actually think that, in general, the Apex Developer Guide is amongst the most well-maintained knowledge bases for code anywhere. In any case, using one "handler" class per SObject is of crucial importance — having seen a few orgs that made the use of multiple Triggers for the same SObject, I can only say that tracking updates and following the code becomes substantially harder if you put logic into your triggers themselves or if your triggers call multiple classes.

Especially after a recent experience helping a friend debug his Mongoose.js / Mongo application, I would submit that great documentation can lift up a sub-par API, yet most API docs I read seem doomed to be afterthoughts, sadly outdated and lacking completeness — dragging down things that might otherwise have been helpful. If there's one thing that Salesforce has done really well over the years, it's been their constant maintenance of the official docs.

I don't think there's a lot of room in the "Trigger Handler" space; if anything, I would simply suggest using Kevin O'Hara's pattern and being done with it. With that being said, there are a few reasons that you might want to go with something considerably more light-weight:

There was one article I read online that, after reviewing Kevin O'Hara's implementation, decided it was too verbose ... then made an interface for their own implementation, forcing any potential consumer to implement all of the trigger methods in order to subscribe. I went for a run after seeing that particular ... suggestion.

linkImplementing The Simplest Possible Trigger Handler

This is the most streamlined Trigger Handler implementation that I could stomach:

1linkpublic virtual class TriggerHandler {

2link @testVisible

3link private static TriggerOperation triggerContext = Trigger.operationType;


5link protected TriggerHandler() {

6link if(!Trigger.isExecuting && !Test.isRunningTest()) {

7link throw new TriggerHandlerException('TriggerHandler used outside of triggers / testing');

8link }

9link }


11link public void execute() {

12link switch on triggerContext {

13link when BEFORE_INSERT {

14link this.beforeInsert(;

15link }

16link when BEFORE_UPDATE {

17link this.beforeUpdate(Trigger.newMap, Trigger.oldMap);

18link }

19link when BEFORE_DELETE {

20link this.beforeDelete(Trigger.oldMap);

21link }

22link when AFTER_INSERT {

23link this.afterInsert(Trigger.newMap);

24link }

25link when AFTER_UPDATE {

26link this.afterUpdate(Trigger.newMap, Trigger.oldMap);

27link }

28link when AFTER_DELETE {

29link this.afterDelete(Trigger.oldMap);

30link }

31link when AFTER_UNDELETE {

32link this.afterUndelete(Trigger.newMap);

33link }

34link }

35link }


37link protected virtual void beforeInsert(List<SObject> newRecords) {}

38link protected virtual void beforeUpdate(Map<Id, SObject> updatedRecordsMap, Map<Id, SObject> oldRecordsMap) {}

39link protected virtual void beforeDelete(Map<Id, SObject> deletedRecordsMap) {}

40link protected virtual void afterInsert(Map<Id, SObject> newRecordsMap) {}

41link protected virtual void afterUpdate(Map<Id, SObject> updatedRecordsMap, Map<Id, SObject> oldRecordsMap) {}

42link protected virtual void afterDelete(Map<Id, SObject> deletedRecordsMap) {}

43link protected virtual void afterUndelete(Map<Id, SObject> undeletedRecordsMap) {}


45link private class TriggerHandlerException extends Exception {

46link }


Every time I use a switch statement now that they're finally out in Apex, I find myself asking the question: "was that brief bit of syntactical sugar really worth the extra lines?" Perhaps not. You could drop a few lines by using our good old if/else paradigm against the Trigger.operationType enum. I shed a single tear of happiness when that enum was released, representative of the years spent looking at Trigger Handler frameworks' boolean comparisons on Trigger.isInsert, Trigger.isDelete, Trigger.isBefore, etc ...

After getting some feedback from a few friends, I've ever dropped the traditional List<SObject> arguments represented by and Trigger.old for everything except beforeInsert, where we naturally don't have access to our Map objects since the records don't have Ids yet.

Here's some similarly slimmed down tests:


2linkprivate class TriggerHandlerTests {

3link // I normally put private classes at the bottom, but to prevent you from having to scroll ...

4link private class TestTriggerHandler extends TriggerHandler {

5link public TriggerOperation Method { get; private set;}


7link @testVisible

8link protected override void beforeInsert(List<SObject> newRecords) {

9link this.Method = TriggerOperation.BEFORE_INSERT;

10link }

11link @testVisible

12link protected override void beforeUpdate(Map<Id, SObject> updatedRecordsMap, Map<Id, SObject> oldRecordsMap) {

13link this.Method = TriggerOperation.BEFORE_UPDATE;

14link }

15link @testVisible

16link protected override void beforeDelete(Map<Id, SObject> deletedRecordsMap) {

17link this.Method = TriggerOperation.BEFORE_DELETE;

18link }

19link @testVisible

20link protected override void afterInsert(Map<Id, SObject> newRecordsMap) {

21link this.Method = TriggerOperation.AFTER_INSERT;

22link }

23link @testVisible

24link protected override void afterUpdate(Map<Id, SObject> updatedRecordsMap, Map<Id, SObject> oldRecordsMap) {

25link this.Method = TriggerOperation.AFTER_UPDATE;

26link }

27link @testVisible

28link protected override void afterDelete(Map<Id, SObject> deletedRecordsMap) {

29link this.Method = TriggerOperation.AFTER_DELETE;

30link }

31link @testVisible

32link protected override void afterUndelete(Map<Id, SObject> undeletedRecordsMap) {

33link this.Method = TriggerOperation.AFTER_UNDELETE;

34link }

35link }


37link @isTest

38link static void it_should_perform_before_insert() {

39link TestTriggerHandler testHandler = new TestTriggerHandler();

40link TriggerHandler.triggerContext = TriggerOperation.BEFORE_INSERT;


42link testHandler.execute();


44link System.assertEquals(TriggerOperation.BEFORE_INSERT, testHandler.Method);

45link }


47link @isTest

48link static void it_should_perform_before_update() {

49link TestTriggerHandler testHandler = new TestTriggerHandler();

50link TriggerHandler.triggerContext = TriggerOperation.BEFORE_UPDATE;


52link testHandler.execute();


54link System.assertEquals(TriggerOperation.BEFORE_UPDATE, testHandler.Method);

55link }


57link @isTest

58link static void it_should_perform_before_delete() {

59link TestTriggerHandler testHandler = new TestTriggerHandler();

60link TriggerHandler.triggerContext = TriggerOperation.BEFORE_DELETE;


62link testHandler.execute();


64link System.assertEquals(TriggerOperation.BEFORE_DELETE, testHandler.Method);

65link }


67link @isTest

68link static void it_should_perform_after_insert() {

69link TestTriggerHandler testHandler = new TestTriggerHandler();

70link TriggerHandler.triggerContext = TriggerOperation.AFTER_INSERT;


72link testHandler.execute();


74link System.assertEquals(TriggerOperation.AFTER_INSERT, testHandler.Method);

75link }


77link @isTest

78link static void it_should_perform_after_update() {

79link TestTriggerHandler testHandler = new TestTriggerHandler();

80link TriggerHandler.triggerContext = TriggerOperation.AFTER_UPDATE;


82link testHandler.execute();


84link System.assertEquals(TriggerOperation.AFTER_UPDATE, testHandler.Method);

85link }


87link @isTest

88link static void it_should_perform_after_delete() {

89link TestTriggerHandler testHandler = new TestTriggerHandler();

90link TriggerHandler.triggerContext = TriggerOperation.AFTER_DELETE;


92link testHandler.execute();


94link System.assertEquals(TriggerOperation.AFTER_DELETE, testHandler.Method);

95link }


97link @isTest

98link static void it_should_perform_after_undelete() {

99link TestTriggerHandler testHandler = new TestTriggerHandler();

100link TriggerHandler.triggerContext = TriggerOperation.AFTER_UNDELETE;


102link testHandler.execute();


104link System.assertEquals(TriggerOperation.AFTER_UNDELETE, testHandler.Method);

105link }


Thanks especially to the ever helpful Suraj Pillai, whose feedback on the original tests helped tighten up this example by showing how you would implement this framework in your actual Triggers -- by instantiating your handler (TestTriggerHandler in this example) and calling execute(). That's it, really; you should never have any more logic than that in your Apex triggers.

linkSyntax Sugar

Lastly, I'll just say that I've only ever found one set of helper methods necessary in the TriggerHandler pattern — it's fairly common to need to get records with changed fields when performing logic within the Handler classes that end up extending the TriggerHandler. You could add the following one/two methods (one if you only wanted to use the bulk List<SObjectField> option) to do yourself a favor:

1link//in TriggerHandler.cls

2linkprotected List<SObject> getUpdatedRecordsWithChangedField(SObjectField field) {

3link return this.getUpdatedRecordsWithChangedFields(new List<SObjectField>{ field });



6linkprotected List<SObject> getUpdatedRecordsWithChangedFields(List<SObjectField> fields) {

7link List<SObject> updatedRecords = new List<SObject>();


9link for(SObject record : {

10link SObject oldRecord = Trigger.oldMap.get(record.Id);

11link for(SObjectField field : fields) {

12link if(record.get(field) != oldRecord.get(field)) {

13link updatedRecords.add(record);

14link }

15link }

16link }

17link return updatedRecords;


linkWrapping Up

That's it! It's a short one, but I hope you enjoyed this post. The aforementioned Writing Performant Apex Tests article has also now been published. The source code can also be browsed as a branch off of my repo!

As well, on the subject of filtering records that have changed based on SObjectField criteria, as shown in the above helper methods, I have since also written a post on the power of Lazy Iteration which should prove eye-opening when considering how to keep your TriggerHandler classes performant in the context of having to assemble many sub-lists of changed SObjects based on different SObjectField criteria. The getUpdatedRecordsWithChangedFields method shown above is a typically-eager implementation, and all of the records being passed to the Trigger get iterated through each time the method is called; if you need to accomplish separate processing for records with different changed fields, you'll quickly waste processing cycles doing so. Lazily implemented iteration prevents this performance slowdown - I'd highly recommend reading the article for more information about this very powerful pattern!

The original version of Lightweight Trigger Handler can be read on my blog.

Implementing The Simplest Possible Trigger HandlerSyntax SugarWrapping Up

Home Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests

Read more tech articles