Read more at jamessimone.net, 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:

classes/TriggerHandler.cls
1linkpublic virtual class TriggerHandler {

2link @testVisible

3link private static TriggerOperation triggerContext = Trigger.operationType;

4link

5link protected TriggerHandler() {

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

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

8link }

9link }

10link

11link public void execute() {

12link switch on triggerContext {

13link when BEFORE_INSERT {

14link this.beforeInsert(Trigger.new);

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 }

36link

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) {}

44link

45link private class TriggerHandlerException extends Exception {

46link }

47link}

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 Trigger.new 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:

classes/TriggerHandler_Tests.cls
1link@isTest

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;}

6link

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 }

36link

37link @isTest

38link static void it_should_perform_before_insert() {

39link TestTriggerHandler testHandler = new TestTriggerHandler();

40link TriggerHandler.triggerContext = TriggerOperation.BEFORE_INSERT;

41link

42link testHandler.execute();

43link

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

45link }

46link

47link @isTest

48link static void it_should_perform_before_update() {

49link TestTriggerHandler testHandler = new TestTriggerHandler();

50link TriggerHandler.triggerContext = TriggerOperation.BEFORE_UPDATE;

51link

52link testHandler.execute();

53link

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

55link }

56link

57link @isTest

58link static void it_should_perform_before_delete() {

59link TestTriggerHandler testHandler = new TestTriggerHandler();

60link TriggerHandler.triggerContext = TriggerOperation.BEFORE_DELETE;

61link

62link testHandler.execute();

63link

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

65link }

66link

67link @isTest

68link static void it_should_perform_after_insert() {

69link TestTriggerHandler testHandler = new TestTriggerHandler();

70link TriggerHandler.triggerContext = TriggerOperation.AFTER_INSERT;

71link

72link testHandler.execute();

73link

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

75link }

76link

77link @isTest

78link static void it_should_perform_after_update() {

79link TestTriggerHandler testHandler = new TestTriggerHandler();

80link TriggerHandler.triggerContext = TriggerOperation.AFTER_UPDATE;

81link

82link testHandler.execute();

83link

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

85link }

86link

87link @isTest

88link static void it_should_perform_after_delete() {

89link TestTriggerHandler testHandler = new TestTriggerHandler();

90link TriggerHandler.triggerContext = TriggerOperation.AFTER_DELETE;

91link

92link testHandler.execute();

93link

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

95link }

96link

97link @isTest

98link static void it_should_perform_after_undelete() {

99link TestTriggerHandler testHandler = new TestTriggerHandler();

100link TriggerHandler.triggerContext = TriggerOperation.AFTER_UNDELETE;

101link

102link testHandler.execute();

103link

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

105link }

106link}

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 });

4link}

5link

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

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

8link

9link for(SObject record : Trigger.new) {

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;

18link}

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