TRIGGER ARCHITECTURE FRAMEWORK RECIPE – SFDCPanther

on

|

views

and

comments

Apex can be invoked by using triggers. Apex triggers enable you to perform custom actions before or after changes to Salesforce records, such as insertions, updates, or deletions.

A trigger is Apex code that executes before or after the following types of operations:

  • insert
  • update
  • delete
  • merge
  • upsert
  • undelete

I have seen the code of many triggers in my career. Some were good, bad and ugly whenever it comes to trigger implementation.

Trigger Best Practices

  1. One Trigger per Object
  2. Logic-less trigger
  3. Context-specific handler methods

I started reading and following the trigger best practices and while reading and implementing the trigger I heard about the trigger framework. In my recent project requirement was to develop the trigger with the framework so being as a lead developer I did took the responsibility to build the lightweight trigger framework.

Why Use a Framework?

Now, we have some best practices you must be thinking that what if I am following all the best practices do I need to use the trigger framework. The simple answer is No.

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
  • Assure the Order of execution of the code
  • Prevent the recursion.
  • Ensure that trigger will do not create any issue while working with large datasets.

I did google, and found many trigger framework, took some ideas and then decided not to use the existing framework and developed my own. I have bellow observation on the existing framework.

  1. Having BeforeInsert(SObject) type method for each event as we all knew that trigger must be bulkified.
  2. Complex TriggerFactory class logic and most of the time process is being executed at runtime which may lead to timeout error.
  3. Complicated design.

Also, time is another barrier that every developer face while implementing the framework.

The framework I developed includes all the best practices with some additional features that are given below :

  1. Ability to control the trigger and its triggering event from the UI.
  2. Prevent recursion based on the field values provided into the Trigger Setting record.
  3. Ability to see which Object trigger is active and in which event trigger will execute using the report.
  4. A custom object that catches the exception and stores the information in details for future reference

The requirement of Trigger Framework

  1. Create Record of Trigger Setting Object for those object you want to build the trigger and this record will be used to control the trigger from the UI.
  2. While developing the handler class developer must implement TriggerInterface and it’s all methods
  3. From the trigger use only line code and pass the handler class instance and the Objectname that you did use while creating the Trigger Setting record.

The Interface – TriggerInterface

The apex interface which contains the methods that needs to be implemented while developing the handler class even if there is no logic for those methods. Using the interface we are assuring that developer will follow all the best practice.

[codesyntax lang=”java” container=”div” doclinks=”1″]

/*
    @Author : Amit Singh
    @BuiltDate : 20th March 2018
    @Description : 
    @Company : sfdcpanther
*/
public interface TriggerInterface{
    
    /*
        @Author : Amit Singh
        @BuiltDate : 20th March 2018
        @Description : Called by the trigger framework before insert of the records
        @params : List<sObject> newRecordsList 
    */
    void beforeInsert(List<sObject> newRecordsList);
    
    /*
        @Author : Amit Singh
        @BuiltDate : 20th March 2018
        @Description : Called by the trigger framework after insert of the records
        @params : List<sObject> newRecordsList , Map<Id, sObject> newRecordsMap
    */
    void afterInsert(List<sObject> newRecordsList , Map<Id, sObject> newRecordsMap);
    
    /*
        @Author : Amit Singh
        @BuiltDate : 20th March 2018
        @Description : Called by the trigger framework before update of the records
        @params : List<sObject> newRecordsList , Map<Id, sObject> newRecordsMap
                  List<sObject> oldRecordsList , Map<Id, sObject> oldRecordsMap
    */
    void beforeUpdate(Map<Id, sObject> newRecordsMap, Map<Id, sObject> oldRecordsMap);
    
    /*
        @Author : Amit Singh
        @BuiltDate : 20th March 2018
        @Description : Called by the trigger framework after update of the records
        @params : List<sObject> newRecordsList , Map<Id, sObject> newRecordsMap
                  List<sObject> oldRecordsList , Map<Id, sObject> oldRecordsMap
    */
    void afterUpdate(Map<Id, sObject> newRecordsMap,  Map<Id, sObject> oldRecordsMap);
    
    /*
        @Author : Amit Singh
        @BuiltDate : 20th March 2018
        @Description : Called by the trigger framework before delete of the records
        @params : List<sObject> newRecordsList , Map<Id, sObject> newRecordsMap
    */            
    void beforeDelete(List<sObject> oldRecordsList , Map<Id, sObject> oldRecordsMap);
    
    /*
        @Author : Amit Singh
        @BuiltDate : 20th March 2018
        @Description : Called by the trigger framework after delete of the records
        @params : List<sObject> newRecordsList , Map<Id, sObject> newRecordsMap
    */
    void afterDelete(Map<Id, sObject> oldRecordsMap);
    
    /*
        @Author : Amit Singh
        @BuiltDate : 20th March 2018
        @Description : Called by the trigger framework after undelete of the records
        @params : List<sObject> newRecordsList , Map<Id, sObject> newRecordsMap
    */
    void afterUnDelete(List<sObject> newRecordsList , Map<Id, sObject> newRecordsMap);
}

[/codesyntax]

The Dispatcher Class – TriggerDispatcher

This class is the heart of the framework which contains 2 methods a) run and b) execute. The developer needs to call the run method from the trigger and pass the parameters then the call will do the trick and will redirect to the correct method of the handler class.

[codesyntax lang=”java” container=”div” doclinks=”1″]

/*
    @Author : Amit Singh
    @BuiltDate : 21st March 2018
    @Description : This class will be called from the Trigger and then will redirect to the correct Handler
    @Company : sfdcpanther
*/
public with sharing class TriggerDispatcher{

    public static void run(TriggerInterface handler, String ObjectName){
    
       /*
        * Fetch the Trigger Settings Records and check in which context Trigger can be executed.
        */ 
        List<Trigger_Setting__c> triggerSettingList = new List<Trigger_Setting__c>();
        triggerSettingList = [Select Id, Name, Disabled__c, Object_Name__c, Before_Update__c, Before_Insert__c, Before_Delete__c,
                                After_Update__c, After_Undelete__c, After_Insert__c, After_Delete__c, Prevent_Recursion__c
                                From Trigger_Setting__c  Where Object_Name__c =: objectName];
        
        execute(handler, triggerSettingList);
    }

    private static void execute(TriggerInterface handler, List<Trigger_Setting__c> triggerSetting){

        if(triggerSetting !=null && triggerSetting.size() > 0){
            if(triggerSetting[0].Disabled__c  ) return ; 
        }else{
            throw new TriggerException('No Trigger Setting found! Please create an entry for '+
                ' Trigger Settings Object. Contact your administrator');
        }
        
        /*
        * If trigger is executing in before context then route to the befor context methods
       */
        if(Trigger.isBefore){
            if(Trigger.isInsert && triggerSetting[0].Before_Insert__c){
                handler.BeforeInsert(Trigger.New);
            }
            if(Trigger.isUpdate && triggerSetting[0].Before_Update__c){
                handler.BeforeUpdate(Trigger.NewMap, Trigger.oldMap);
            }
            if(Trigger.isDelete && triggerSetting[0].Before_Delete__c){
                handler.BeforeDelete(Trigger.old, Trigger.oldMap);
            }
        }
        
       /*
        * If trigger is executing in after context then route to the after context methods
        */
        If(Trigger.isAfter){
            if(Trigger.isInsert && triggerSetting[0].After_Insert__c){
                handler.AfterInsert(Trigger.new, Trigger.newMap);
            }
            
            /* If trigger is executing in After Update Context then Check 
               if the field have been changed or not if not do not call the hanlder
               in order to prevent the recursion
           */
           
            If(Trigger.isUpdate && triggerSetting[0].After_Update__c){
                Map<Id, sObject> newItemsMap = new map<Id, sObject>();
                Map<Id, sObject> oldItemsMap = new map<Id, sObject>();
                List<String> fieldAPINameList = new List<String>();
                if(triggerSetting[0].Prevent_Recursion__c !=null)
                    fieldAPINameList = triggerSetting[0].Prevent_Recursion__c.split(',');
                
                for(sObject obj : Trigger.NewMap.Values()){
                    for(String field : fieldAPINameList){
                        if(obj.get(field.trim()) != Trigger.oldMap.get(obj.Id).get(field.trim())){
                            
                            if(!newItemsMap.containsKey(obj.Id))
                                newItemsMap.put(obj.Id, obj);
                            if(!oldItemsMap.containsKey(obj.Id))
                                oldItemsMap.put(obj.id, Trigger.oldMap.get(obj.Id));
                        }
                    }
                }
                handler.AfterUpdate(newItemsMap, oldItemsMap);
            }
            If(Trigger.isDelete && triggerSetting[0].After_Delete__c){
                handler.AfterDelete(Trigger.oldMap);
            }
            If(Trigger.isUndelete && triggerSetting[0].After_Undelete__c){
                handler.AfterUndelete(Trigger.new, Trigger.newMap);
            }
        }
    }
    
}

[/codesyntax]

In the above class, we are making a SOQL on Trigger Setting by which we will control the trigger events and also trigger is disabled or Not.  if(triggerSetting[0].Disabled__c ) return; checks if the trigger is disabled from the UI if yes then return the trigger and do not execute.

Develop the handler class – Example (AccountTriggerHandler)

The developer needs to develop the handler class and must implement the TriggerInterface with it’s all methods. Here is the sample handler class. Implementation of the TriggerInterface interface is must.

In the handler class method use try and catch where required and in catch method call doHandleException method of TransactionLogHandler class to catch the exception.

[codesyntax lang=”java” container=”div” doclinks=”1″]

/*
    @Author : Amit Singh
    @BuiltDate : 21st March 2018
    @Description : Handler class that will be created by developer and allowed only one for every object
    @Company : sfdcpanther
*/
public class AccountTriggerHandler implements TriggerInterface{
 
    public void BeforeInsert(List<SObject> newItems) {
        /* Update the Account name before account inserted into Salesorce */
        for(Account acc : (List<Account>)newItems){
            acc.Name = 'Trigger FrameWork ' +acc.Name;
        }
    }
 
    public void BeforeUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {}
 
    public void BeforeDelete(List<sObject> oldRecordsList , Map<Id, SObject> oldItems) {}
 
    public void AfterInsert(List<sObject> newRecordsList , Map<Id, SObject> newItems) {
        Try{
        }Catch(System.Exception ex){
            /* Call the TransactionLogHandler class method to create a log 
               parameters need to pass in the method are System.Exception and the Handler ClassName
            */
            TransactionLogHandler.doHandleException(ex , 'AccountTriggerHandler');
        }
    }
 
    public void AfterUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {
        /* Update the AccountNumber */
        List<Account> accountToUpdateList = new List<Account>();
        Try{
           for(Account acc : [Select Id, Name, AccountNumber From Account Where Id =: newItems.keySet()]){
               acc.AccountNumber = '123345543625';
               accountToUpdateList.add(acc);
           }
           /* Update Account and Test if the Recursion is Being prevented */
           update accountToUpdateList;
           
        }Catch(System.Exception ex){
            /* Call the TransactionLogHandler class method to create a log 
               parameters need to pass in the method are System.Exception and the Handler ClassName
            */
            
            TransactionLogHandler.doHandleException(ex , 'AccountTriggerHandler');
        }
    }
 
    public void AfterDelete(Map<Id, SObject> oldItems) {}
 
    public void AfterUndelete(List<sObject> newRecordsList , Map<Id, sObject> newItems) {}
    
}

[/codesyntax]

Built the trigger and call the dispatcher class –

In the final steps, create the trigger and put a single line of code and rest is magic. See example trigger on account object.

[codesyntax lang=”java” container=”div” doclinks=”1″]

/*
    @Author : Amit Singh
    @BuiltDate : 21st March 2018
    @Description : Account Trigger that will call the RUN mthos of TriggerDispatcher class
    @Company : sfdcpanther
*/

trigger AccountTrigger on Account (before insert, after insert, before update, after update, before delete, after delete, after undelete) {
    TriggerDispatcher.run(new AccountTriggerHandler() , 'Account');
}

[/codesyntax]

In the trigger code, the developer needs to call run method of TriggerDispatcher with 2 parameters

  1. The instance of the Handler class
  2. The name of the object of the Trigger Setting for the same Object. For example, we are building the trigger on account object then ObjectName in Trigger Setting object will be account and then we need to pass Account as the second parameter while calling the method.

The Log handler class – TransactionLogHandler

The class which is responsible for catching the exception and then inserting into the Transaction Log Object.

Here is the code

[codesyntax lang=”java” container=”div” doclinks=”1″]

/*
    @Author : Amit Singh
    @BuiltDate : 20th March 2018
    @Description : to create the Transaction Log record if there is any Error while processing !!
    @Company : sfdcpanther
*/

public with sharing class TransactionLogHandler{

    public static void doHandleException(System.Exception ex , String processName){
        Transaction_Log__c transactionLog = new Transaction_Log__c(
            Error_Log__c = ex.getStackTraceString() +'<br/>' + ex.getMessage() + '<br/>' + ex.getCause() +' <br/>'+ ex.getTypeName(),
            Exception_Time__c = System.Now(),
            Process_Name__c = processName,
            Class_Name__c = processName
        );
        if(Schema.sObjectType.Transaction_Log__c.isCreateable()){
            insert transactionLog;
        }
        
    }

}

[/codesyntax]

Control the Trigger From UI –

The best part of the Framework is you can control the trigger from the UI. Open the Trigger Setting for the Object and you can select whether the trigger is disabled and on which events trigger will execute

See the report –

Open “Object Trigger Settings” where you can see the report of the trigger in single place and many important information like the trigger is active?. Events in which trigger will execute.

We simply call the static method on the TriggerDispatcher and pass it a new instance of our AccountTriggerHandler. The framework takes care of the rest.

The advantage of using Trigger Framework: –

  1. Help you conform the best practices.
  2. Control trigger from UI.
  3. Prevent recursion.
  4. Simplify testing and maintenance of your application logic
  5. Assure the Order of execution of the code

Entity Relationship Diagram (ERD)

If you’re planning to implement a trigger framework but don’t have the time to implement a more complex solution, then you should probably start with a framework like this and build on it yourself. You can always add new functionality as and when it is required.

Install Framework From Here

On the other hand, you are NOT considering a trigger handler framework, you should reconsider!

Any questions or comments? Feel free to post them here or reach me on Twitter.

Share with your friends and colleagues because sharing is caring:). Also, like the facebook page to get the latest updates.

Resources that I did look at

  1. Hari Krishnan’s Blog
  2. Trigger Frameworks and Apex Trigger Best Practices
  3. Chris Aldridge’s Blog
Amit Singh
Amit Singhhttps://www.pantherschools.com/
Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion
Share this

Leave a review

Excellent

SUBSCRIBE-US

Book a 1:1 Call

Must-read

How to Utilize Salesforce CLI sf (v2)

The Salesforce CLI is not just a tool; it’s the cornerstone of development on the Salesforce Platform. It’s your go-to for building, testing, deploying, and more. As one of the most important development tools in our ecosystem

Save the day of a Developer with Apex Log Analyzer

Table of Contents What is Apex Log Analyzer? Apex Log Analyzer, a tool designed with Salesforce developers in mind, is here to simplify and accelerate your...

Salesforce PodCast

Introduction Hey Everyone, Welcome to my podcast, the first-ever podcast in India for Salesforce professionals. Achievement We are happy to announce that we have been selected as Top...

Recent articles

More like this

14 COMMENTS

  1. Trigger is using triggersettings object. The relationship is HAS A and not IS A, which is not a best practice if Object oriented programming. And test classes will not be able to discover when the data in trigger settings object is corrupted.

  2. If we have multiple record types we want to write different triggers but at a time only one should execute how do we achieve using your framework

    • As per the Salesforce best practice we need to maintain only one Trigger per Object. But still, if you want to achieve the above functionality then you can make some tweaks to the code and Trigger Setting Object.

      1 – Create a Field on Trigger Setting Object which will control the record type for Account Object.
      2 – Make the change in the Trigger Dispatcher class which will look into the trigger setting object and then dispatch to the associated handler class method.

      Regards,
      SFDCPanther

      • The main problem which we face using above approach is that too much time is consumed by salesforce to know which trigger class to call.
        Also its like this Dev Team A is using A record type ,Dev Team B is using B record type so both team’s code should be in different classes.

        • In this case Both development team must be writing the different classes. You can add a Map there with Record Type Id as Key and Class name a value. Then use this map to redirect to the correct class. This will also be easy to maintain in future,

  3. great post. thank you for sharing.

    If you are having several methods for each trigger event how do you call the methods properly under each event method. I was able to do it correctly for before and after update but not for inserts. This is an example of what I mean and what worked for me. BeforeInsert doesn’t work because “newItems” variable doesn’t exist. BeforeUpdate works.

    public void BeforeInsert(List newItems) {

    AccountTask(newItems);

    }

    public void BeforeUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {

    AccountTask(newItems,oldItems);

    }

    public void AccountTask(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {
    //code
    }

    • It is a similar way that we do call. One thing that you need to consider is to create a Helper and then call the methods from the Handler. You can pass the value that you get into the handler or you can directly access the Trigger context variable into the class.

  4. HI Amit,

    I tried to install your trigger Framework in my Developer Org but the installation is failing with errors pointing to TriggerDispatcherTest class.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

5/5

Stuck in coding limbo?

Our courses unlock your tech potential