A simple Apex trigger framework

Whether one should use an Apex trigger framework or not is probably worth a separate discussion since any abstraction is at a cost and the cost could outweigh the benefits of the framework prematurely introduced. For the case that little business logic needs to be managed in triggers, a general practice is to keep it simple stupid (KISS). However Apex trigger frameworks have still been discussed in many developer forums and Salesforce programming books, focusing on organising the code to deal with the more complex domain problems. Many of these frameworks/patterns focus too much on putting an abstraction over the combinations of the trigger stages (before and after) and the operation types (Insert, Update, Delete, Undelete). That normally results in lots of boilerplate code to maintain. Some frameworks/patterns, introducing several interfaces to implement, are not that inviting to even get started with. This post presents an Apex trigger framework (aka. trigger handler pattern) that aims to separate trigger concerns, reduce programmer errors, and improve the modularity while maintaining a simple style.

(The more meaningful compiled code can be found in this GitHub repo.)

There are these main concerns in Apex triggers:

  • Multiple triggers can be defined for the same object and their execution order is not guaranteed.
  • The before and after stages.
  • Trigger operations: isInsert, isUpdate, isDelete, isUndelete.
  • Individual trigger processes are often change-based, i.e. only executed on certain records that have some change.
  • Individual trigger processes may need to be switched on/off.
  • Trigger logic mostly deals with a domain problem so the core logic could be executed else where – such as Apex REST API or a batch job.

When multiple triggers are defined for the same object, code gets complex to debug as developers need to be aware of all its triggers. Since the execution order of the triggers is not guaranteed, multiple before triggers (or after triggers) are in contention with each other. This makes things worse. It’s a widely accepted pattern to have one trigger per object. Further to this, keeping triggers thin has the benefit of leveraging Apex classes to organise the trigger logic. The following code shows how an AccountTrigger is written in such a style. It simply delegates its work to the common TriggerHandler class.

trigger AccountTrigger on Account (before insert, before update, 
before delete, after insert, after update, after delete, after undelete) {
    TriggerHandler.handle(TriggerConfig.ACCOUNT_CONFIG);
}

By looking at the name of the classes, one can tell that only the ACCOUNT_CONFIG is specific to the AccountTrigger. Everything else is common in all triggers. One line per trigger per object looks neat. Note that the typical trigger stages, operations, and their corresponding context variables like Trigger.isBefore, Trigger.isInsert, Trigger.newMap are not concerned here at all.

It’s tempting to put an abstraction on the permutation of the stage factor (before and after) and the operation factor (insert, update, etc.). That would result in lots of boilerplate code (how often do you need to handle a beforeUndelete event?). Quite often, the same logic needs to be invoked in both isInsert and isUpdate operations, e.g. do something when a Status field is changed to “Approved” no matter if it is a new record with “Approved” status or is an existing record that has status changed to “Approved”. Rather, The before and after stages have their distinctive purposes. Developers often need to think about carefully if the new trigger logic should be put into the before or after trigger. Normally the logic should be in either stage, very unlikely in both. Therefore, separating the before and the after concerns is more useful to remove design errors. The TriggerHandler class is common in every trigger. It focuses on these two stages and leaves the handling of the operation type to each specific trigger operation. The code is shown as follows:

/**
 * The common trigger handler that is called by every Apex trigger.
 * Simply delegates the work to config's before and after operations.
 */
public with sharing class TriggerHandler {
    public static void handle(TriggerConfig config) {
        if (!config.isEnabled) return;
        
        if (Trigger.isBefore) {
            for (TriggerOp operation : config.beforeOps) {
                run(operation);
            }
        }
        
        if (Trigger.isAfter) {
            for (TriggerOp operation : config.afterOps) {
                run(operation);
            }
        }
    }
    
    private static void run(TriggerOp operation) {
        if (operation.isEnabled()) {
            SObject[] sobs = operation.filter();
            if (sobs.size() > 0) {
                operation.execute(sobs);
            }
        }
    }
}

Let’s have a look at the TriggerOp interface (“TriggerOperation” is already used by Salesforce). It represents an individual trigger operation that encapsulates some relatively independent business logic.

public interface TriggerOp {
    Boolean isEnabled();
    SObject[] filter();
    void execute(SObject[] sobs);
}

It is important to guard the execution of the logic by checking a condition – which is often the operation types such as Trigger.isInsert, Trigger.isUpdate. Here, the isEnabled() method, if needed, can also merge other flags to allow in-memory switches to turn on/off the operation or to link to a custom setting or a static resource. Another concern developers have is that not all records should be applied with the logic. Normally there should be a check to guard only the records that have a change. Thus the filter() method enforces developers to think about this aspect for if it got dismissed, it would result in some complex trigger recursive calls. If all records need to be processed, the implementation class can simply return all records in the Trigger.new list.

In terms of how the common TriggerHandler handles various different trigger operations, it is the TriggerConfig that addresses these common concerns:

  • The setting to enable/disable the trigger
  • The operations in relation to the before and after stages

The following is the TriggerConfig class that shows various different configurations for different object triggers. It statically instantiates many TriggerConfig objects, each of which is ready to be used in their own trigger.

/**
 * A singleton class that presents the configuration properties of the individual triggers.
 */
public inherited sharing class TriggerConfig {
    public Boolean isEnabled {get; set;}
    public TriggerOp[] beforeOps {get; private set;}
    public TriggerOp[] afterOps {get; private set;}
    
    public static final TriggerConfig ACCOUNT_CONFIG = new TriggerConfig(
        	new TriggerOp[] {new AccountTriggerOps.OperationA()},
        	new TriggerOp[] {new AccountTriggerOps.OperationB()});
    // Other object trigger config
    
    private TriggerConfig(TriggerOp[] beforeOps, TriggerOp[] afterOps) {
        this.isEnabled = true;
        this.beforeOps = beforeOps;
        this.afterOps = afterOps;
    }
}

The above code can be further tweaked to dynamically instantiate TriggerConfig records from a JSON static resource so as to further decouple from the individual TriggerOp implementations. See this GitHub repo for more details.

The AccountTriggerOps class is simply a superset of all TriggerOp(s) in relation to the Account, organised in a top-level class:

public with sharing class AccountTriggerOps {
    public class OperationA implements TriggerOperation {
        public Boolean isEnabled() {
            return Trigger.isInsert || Trigger.isUpdate;
        }
        
        public SObject[] filter() {
            return Trigger.new;
        }
        
        public void execute(Account[] accounts) {
            // validation logic
        }
    }

    public class OperationB implements TriggerOp {
        public Boolean isEnabled() {
            return Trigger.isUpdate;
        }
        
        public Account[] filter() {
            Account[] result = new Account[] {};
            for (Account newAccount : (Account[]) Trigger.new) {
                Account oldAccount = (Account) Trigger.oldMap.get(newAccount.Id);
                if (oldAccount.Status__c != 'Active' && newAccount.Status__c == 'Active')  {
                    result.add(newAccount);
                }
            }
            return result;
        }

        public void execute(Account[] changedAccounts) {
            Set<Id> statusChangedIds = new Set<Id>();
            for (Account acc : changedAccounts) {
                statusChangedIds.add(acc.Id);
            }
            new AccountChangeStatusBatchable(accountIds).run();
        }
    }

    public class OperationC implements TriggerOperation {
        ......
    }

    public class OperationD implements TriggerOperation {
        ......
    }
    
}

The context variables (such as Trigger.old, Trigger.newMap) are only referenced directly in each TriggerOp as only each individual trigger operation knows which condition (Trigger.isInsert, Trigger.isUpdate, etc.) the logic should be executed. This decides which trigger context variables to use.

This framework, if adopted in a managed package, has the potential to open for extension, i.e. having the TriggerOp defined as a global interface. Then individual TriggerOp implementation classes can be specified in a static resource for each TriggerConfig. In theory, the custom code within an org that installs the managed package can hook their own trigger operations into the managed package’s trigger execution order by specifying the individual TriggerOp(s) to run in the a static resource.

In summary, this Apex trigger framework provides these benefits:

  • Allowing each trigger to be individually switched on/off.
  • Allowing each trigger operation to be individually switched on/off.
  • Promoting consideration of the before and after stages where the logic should belong to.
  • Promoting consideration of the changed records that need to be processed.
  • Increased modularity on managing the code.
  • Simple to use (well, subject to the definition of “simple”).

Published by Chun Wu

When nothing is certain, anything is possible.

Leave a comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: