The Salesforce Trigger Handler Framework

Share this article...

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. Tech Debt: The Silent Org Killer – Chandler Anderson
  16. “Just Because It’s Possible to Write Code, Doesn’t Mean You Should Write Code” – Parker Harris
  17. Last but not least, “Triggers are the original sin on Salesforce.” – Siddhesh Kabe

9 thoughts on “The Salesforce Trigger Handler Framework

  1. 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.

    1. 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.

  2. 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.

  3. 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.

  4. 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

    1. 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.

  5. 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

    1. 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.

  6. Can you give an example of a helper function? Like updating account phone number to 222-222-2222 on insert? Thanks

Add Comment