Developers

The Salesforce Trigger Handler Framework

By Shumon Saha

A Trigger is an Apex script that executes before or after data manipulation events, such as before or after records insert, update, or delete. Triggers are written to perform tasks that can’t be done by using point-and-click tools in Salesforce. Salesforce newbies often jump into writing multiple triggers on the same object. Later, they realize that they can’t control which trigger runs first. A simple Trigger Handler Framework can control the order of execution. It’s simply a rule of keeping strictly one trigger per object.

Likewise, when onboarding an existing Salesforce implementation, document the number of triggers per object, as a part of its Health Check, and decide if you want to remediate it. While we’re at it, let’s also design our triggers with a universal on/off checkbox. By utilizing this tick box, we can turn on/off all triggers in one click. It’s helpful because triggers are often required to be turned off during data loading.

The Tick Box

Go to: Setup → Platform Tools → Custom Code → Custom Metadata Types → New button

Label: Org-Specific Setting

Plural Label: Org-Specific Settings

Starts with vowel sound: Ticked

Object Name: Org_Specific_Setting

… and Save.

Next, click the “New” button on Custom Fields for adding the on/off tick box.

Field Type: Checkbox

Field Label: Value

Default Value: Checked

Field Name: Value

… and Save.

Next, to add your first record, click the “Manage” button.

After entering the “Manage” page, click the “New” button and enter a new CMT record as follows:

Label: Run All Triggers

Org-Specific Setting Name: Run_All_Triggers

Value: Ticked

… and Save.

You can add more records down the line, for example, “Run All Validation Rules”.

The Single Trigger Per Object

Here is the single trigger for the Account object. Don’t forget to uncomment your required lines of code.

trigger AccountTrigger on Account (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
    if (Org_Specific_Setting__mdt.getInstance('Run_All_Triggers')?.Value__c == true) {
        TriggerHandler handler = new AccountTriggerHandler(Trigger.isExecuting, Trigger.size);
        switch on Trigger.operationType {
            when BEFORE_INSERT {
                // handler.beforeInsert(Trigger.new);
            } 
            when BEFORE_UPDATE {
                // handler.beforeUpdate(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap);
            }
            when BEFORE_DELETE {
                // handler.beforeDelete(Trigger.old, Trigger.oldMap);
            }
            when AFTER_INSERT {
                // handler.afterInsert(Trigger.new, Trigger.newMap);
            }
            when AFTER_UPDATE {
                // handler.afterUpdate(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap);
            }
            when AFTER_DELETE {
                // handler.afterDelete(Trigger.old, Trigger.oldMap);
            } 
            when AFTER_UNDELETE {
                // handler.afterUndelete(Trigger.new, Trigger.newMap);
            }
        }
    }
}

Here is a screenshot from my org, however, you won’t be able to save the Trigger on your org yet because its dependent Handler class is pending.

The Trigger Handler Interface

An interface (not related to user interfaces) is like a class in which none of the methods have been implemented – the method signatures are there, but the body of each method is empty. To use an interface, another class must implement it by providing a body for all of the methods contained in the interface.

Interfaces enforce developers to follow the same blueprint. So there should be only one TriggerHandler Interface in the whole org; not one per object.

public interface TriggerHandler {
    void beforeInsert(List<SObject> newRecords);
    void beforeUpdate(List<SObject> oldRecords, List<SObject> newRecords, Map<ID, SObject> oldRecordMap, Map<ID, SObject> newRecordMap);
    void beforeDelete(List<SObject> oldRecords, Map<ID, SObject> oldRecordMap);
    void afterInsert(List<SObject> newRecords, Map<ID, SObject> newRecordMap);
    void afterUpdate(List<SObject> oldRecords, List<SObject> newRecords, Map<ID, SObject> oldRecordMap, Map<ID, SObject> newRecordMap);
    void afterDelete(List<SObject> oldRecords, Map<ID, SObject> oldRecordMap);
    void afterUndelete(List<SObject> newRecords, Map<ID, SObject> newRecordMap);
}

You can save the Interface, but you won’t be able to save the Trigger yet.

The Trigger Handler

Here is the Trigger Handler for the Account object. Don’t forget to uncomment your required lines of code.

public without sharing class AccountTriggerHandler implements TriggerHandler {
    private boolean triggerIsExecuting;
    private integer triggerSize;
    public AccountTriggerHelper helper;
    public AccountTriggerHandler(boolean triggerIsExecuting, integer triggerSize) {
        this.triggerIsExecuting = triggerIsExecuting;
        this.triggerSize = triggerSize;
        this.helper = new AccountTriggerHelper();
    }
    public void beforeInsert(List<Account> newAccounts) {
        // helper.doTask1();
        // helper.doTask2();
    }
    public void beforeUpdate(List<Account> oldAccounts, List<Account> newAccounts, Map<ID, SObject> oldAccountMap, Map<ID, SObject> newAccountMap) {
        // helper.doTask3();
        // helper.doTask4();
    }
    public void beforeDelete(List<Account> oldAccounts, Map<ID, SObject> oldAccountMap) {
        // helper.doTask5();
        // helper.doTask1();
    }
    public void afterInsert(List<Account> newAccounts, Map<ID, SObject> newAccountMap) {
        // helper.doTask2();
        // helper.doTask3();
    }
    public void afterUpdate(List<Account> oldAccounts, List<Account> newAccounts, Map<ID, SObject> oldAccountMap, Map<ID, SObject> newAccountMap) {
        // helper.doTask4();
        // helper.doTask5();
    }
    public void afterDelete(List<Account> oldAccounts, Map<ID, SObject> oldAccountMap) {
        // helper.doTask3();
        // helper.doTask1();
    }
    public void afterUndelete(List<Account> newAccounts, Map<ID, SObject> newAccountMap) {
        // helper.doTask4();
        // helper.doTask2();
    }
}

Here is a screenshot from my org; however, you again won’t be able to save the Handler class in your org yet because its dependent Helper class is pending.

The Trigger Helper

This is where you write your small bite-sized tasks (not related to computer bytes). Rename the functions as required.

public without sharing class AccountTriggerHelper {
    public AccountTriggerHelper() {
        System.debug('Inside AccountTriggerHelper Constructor');
    }
    public void doTask1() {
        System.debug('Inside Task 1');
    }
    public void doTask2() {
        System.debug('Inside Task 2');
    }
    public void doTask3() {
        System.debug('Inside Task 3');
    }
    public void doTask4() {
        System.debug('Inside Task 4');
    }
    public void doTask5() {
        System.debug('Inside Task 5');
    }
}

Now you can finally save all files (Helper + Interface + Handler + Trigger). Edit the Helper, rename the functions, and write your modular tasks.

Test Your Changes

Don’t forget to uncomment your required lines of code before testing.

You can test it as a normal day at work within the Account tab while having the Developer Console open in another browser tab.

Creating a new Account record calls handler.beforeInsert() and handler.afterInsert().

Editing and saving an Account record calls handler.beforeUpdate() and handler.afterUpdate().

Clicking “Delete” on an Account record calls handler.beforeDelete() and handler.afterDelete().

Lastly, restoring a deleted Account record from the Recycle Bin only has one corresponding trigger event – AFTER_UNDELETE – and calls handler.afterUndelete().

Test Classes

Don’t forget to write your test classes immediately, and don’t delegate them to the unsuspecting intern.

We are unable to provide test classes because we wouldn’t know your required business case. Test classes are not supposed to sweep code in order to increase code coverage. Test classes are supposed to trace actual business processes and as a result, will show you unused code to delete.

Summary

So what have we learned?

  • Incorporate the Trigger Handler Framework from the start only if you have to write a trigger.
  • More than one trigger on the same object leads to a random order of trigger execution.
  • When onboarding an existing Salesforce implementation, document the number of triggers per object, as a part of its Health Check, and decide if you want to remediate it.
  • We can control the order of execution by having strictly one trigger per object. For example, AccountTrigger.
  • This Trigger will intercept all Account events, and call corresponding Handler functions.
  • The Handler functions call bite-sized business processes in the Helper Class.
  • The order of trigger execution is now controlled by you.
  • Use an Apex Interface to enforce all Trigger Handlers to follow the same blueprint.
  • Design your triggers to be disabled by unticking the “Run All Triggers” record in Custom Metadata Types. Disabling is often required during data loading. Ensure that you don’t delete the CMT record.
  • Lastly, don’t forget to uncomment the required lines of code before use.

References

  1. Trigger Context Variables
  2. Safe Navigation Operator
  3. When is a Boolean not a Boolean?
  4. Rethink Trigger Logic with Apex Switch
  5. Switch Statements
  6. Enums
  7. TriggerOperation Enum
  8. Interfaces
  9. Lists and Maps
  10. Using the this Keyword
  11. Introduction to Custom Metadata Types
  12. Creating an On-off Power Switch for All Org Automations
  13. The Custom Setting Every Administrator Needs
  14. Amit Sharma on the Trigger Handler Framework
  15. “Just Because It’s Possible to Write Code, Doesn’t Mean You Should Write Code” – Parker Harris
  16. Last but not least, “Triggers are the original sin on Salesforce.” – Siddhesh Kabe

The Author

Shumon Saha

Shumon is a Salesforce Certified Application Architect.

Comments:

    Evan Ponter
    February 28, 2022 7:40 pm
    This is amazing. I've been wading into the world of apex and have been frustrated with finding a clean, simple way to organize code. When I look up trigger frameworks, the analysis usually goes way over my head. Everything I'm seeing here is accompanied with a working example and shows me why each piece is important. Well done! If I have a class (sort of a utility function I suppose) that accepts parameters for a variety of objects, would you call that utility class directly from the trigger handler or via the helper class? Thanks again for your thoughts.
    Callum
    February 28, 2022 7:50 pm
    The title of this article is a bit misrepresentative. This is a simple trigger handler framework, not the only one that is out there. There are a huge number of open source trigger framework projects out there, most of which are both simple to utilise and provide more functionality. Even Salesforce Program Architects are recommending the use of open source rather than re-inventing the wheel yourself.
    Shumon
    March 01, 2022 6:41 pm
    Thank you, Evan. It really depends on your intuition if you need the helper class. You might want to keep things simple, call the utility class directly from the handler, and omit the helper. Or if you need to call the same but numerous utility functions for multiple trigger operation types, you might need the helper. Or you simply might want to enforce uniformity in your org and always (or never) have a helper class.
    sean fielding
    March 04, 2022 3:52 pm
    Lots of great advice in this article, but agree that this is not the only trigger framework. A couple of areas I also found useful to include in a trigger architecture. - Ordering - The ability to change the order of execution in a trigger framework. - Individual Method Toggle - The ability to toggle individual methods on an off as needed. - Recursion Block - The ability to prevent recursion at the method. - Debugging Toggle - The ability to log specific debugging at a method level.
    Madhuri
    March 28, 2022 11:59 am
    Hi I am new to development can you please suggest me how to write a apex handler and trigger on: i have complaint object and Account object ,in Complaint object i have field approver this should be updated with the Account object feild Ownerid when status(complaint object) is open . thanks in advance
    Shumon
    March 28, 2022 10:24 pm
    Hi Madhuri — a Formula Field should be sufficient. For some situations, a Flow may be considered, but not a Trigger — no code. Also it'd be good to check if it's possible to use the standard Case object instead of a custom Complaint object. The more custom objects and relations, the more convoluted an org becomes.
    Madhuri
    March 29, 2022 2:12 pm
    Thank You Shumon, but it was a business requirement to update using apex handler and trigger actually i have got three tasks related to same apex handler and trigger 1. ownerid should be automatically populated in approver field 2. complaint assignee automatically be updated in complaint team to see complain in read only 3. close all sub tasks before status of task is closed (whatid = recordid) else display error message
    Shumon
    April 02, 2022 9:41 am
    Hi Madhuri — business requirements shouldn't enforce the use of a Trigger. Regarding the three queries: 1. A Formula Field should suffice, or a Flow may be considered for some situations. 2. "Case Teams" and "Case Assignment Rules" fit best if the Case object can be used. A custom Complaint object doesn't sound like an optimum design decision. 3. Sub-Tasks aren't available in Salesforce, yet.
    Spyro
    April 26, 2022 1:17 am
    Can you give an example of a helper function? Like updating account phone number to 222-222-2222 on insert? Thanks
    gabi
    July 21, 2022 6:41 pm
    Why not use Trigger.isBefore, Trigger.IsUpdate , etc ? You just rebuilt all the functionality that is out of the box with those methods that I mentioned.
    Harish
    July 26, 2022 5:53 pm
    Hi Shumon, Nice article. Quick query - Is there any design decision taken as the trigger handler and helper are running in without sharing mode. Ideally, from a security standpoint, we need to run the class in user mode, right?
    Shumon
    August 05, 2022 6:05 pm
    Hi Gabi — enums (BEFORE_UPDATE, etc.) are also out-of-the-box and look cleaner than Trigger.isBefore && Trigger.IsUpdate. Check it out here — https://developer.salesforce.com/blogs/2018/05/summer18-rethink-trigger-logic-with-apex-switch
    Siddesh Pande
    October 10, 2022 1:41 pm
    How to pass context variables from TriggerHandler to TriggerHelper. when i pass it gives random errors
    rohit
    February 10, 2023 12:23 pm
    Hi Shumon, On high level is the order of execution like Single trigger-> Handler -> Helper -> interface OR Handler -> Helper -> Trigger -> Interface I just wanted to know when coding from where it starts
    Shumon Saha
    July 20, 2023 9:44 pm
    Hi Spyro, Helper functions are most useful when permutations & combinations of small tasks are needed to be done for multiple types of Trigger events (such as BEFORE_INSERT, BEFORE_UPDATE, etc.). For example, you can write one helper function to update the account phone number and call this helper function from handler.beforeInsert and handler.beforeUpdate, along with other small helper tasks.
    Shumon Saha
    July 20, 2023 9:48 pm
    Thank you Harish, I had a quick chat in the office and yes you are right. From a security standpoint, initially it’s best to make the handler & helper classes “with sharing” to run in user mode.
    Shumon Saha
    July 20, 2023 9:50 pm
    Hi Siddesh, you can pass Trigger Context Variables from the Trigger to the Handler and then to the Helper either via constructors or via function parameters. I did it recently as well.
    Shumon Saha
    July 20, 2023 9:51 pm
    Hi Rohit, the Interface is not a part of the order of execution, it is simply a list of methods. The order of execution is Trigger → Handler (implements Interface) → Helper. But when coding, you have to first write the Interface → Helper → Handler → Trigger.

Leave a Reply