Salesforce Trigger Handlers Driven by Custom Metadata

by Aidan Harding - March 23, 2018

Salesforce has a number of recommendations regarding best practice when writing Apex triggers. Two of these recommendations are:

One Trigger per object write a single trigger which then invokes various Apex classes to do the actual work of what could otherwise be a collection of triggers. If you write multiple triggers, you cannot specify the order that they will run in. By having a single trigger, you can decide what the order of execution will be.

Logic-less triggers the trigger should have as little logic as possible, delegating all the real work to Apex classes. This helps to encourage other good practices – such as being able to share the logic from the trigger with other code (e.g. so that you could invoke a batch process to effectively run the trigger on historical records).

Salesforce even propose a solution to adopt these best practices, by writing trigger handler. I never particularly liked their solution because it leads to a central trigger handler class for each object which then needs to be modified every time you want to add a new “trigger” to the object.

My Solution

I wanted to come up with a system where adding a new trigger did not require modifying any existing code. I want to avoid repetitive code – If you’re copy + pasting a template, and modifying bits here and there, then you’re probably missing a chance to write a more generic piece where the code never changes. In Object Oriented terms, the handler should be open for extension and closed for modification.

So, my solution to organising triggers is to write a single trigger managing class which can be invoked in the same way from a single trigger on any object:

trigger ContactTrigger on Contact (before insert, before update,

before delete, after insert, after update, after delete, after undelete) {
(new MetaDataTriggerManager()).handle();
}

The trigger manager is configured using Custom Metadata. It comes with a set of interfaces, guaranteeing which methods the class which implements the trigger logic says it will provide. An example trigger handler would look like this:

The final step in joining everything up is to add a custom metadata record that tells the trigger manager that it needs to run ContactMyTrigger on AfterUpdate for Contact:

Key Benefits

All the usual benefits of taking your logic out of the trigger

Guaranteed order-of execution for triggers

Adding a new “trigger” can be done by adding to your code and metadata, not modifying anything that already exists

The metadata is a chance to provide some basic admin-visible documentation

The metadata provides the option of disabling a trigger (clearly, this should be used with care for triggers which are intended to enforce invariants in the data)

Triggers can be deployed along with the metadata that configures their usage

Since custom metadata is visible to tests, unit test are automatically configured to run all the same triggers as normal system execution

Dynamic instantiation inside the trigger manager

The trigger manager itself relies on being able to dynamically create an instance of a class by knowing its name from the custom metadata. Then, we need to be able to dynamically invoke the correct handler method. Since the metadata also includes the trigger event that we need to run on, we know which interface the trigger handler ought to have implemented. So, we can cast the instance of the trigger handler to the right interface and call the handler on it. Overall, it looks like this:

The call to Type.forName gets a reference to the type of the class that was provided by the metadata. Later, we use that type to create a new instance, cast it to the BeforeUpdate interface, and call handleBeforeUpdate() .

Everything else in the manager is pretty simple.

Summing up and link to source

At Nebula, we have this trigger manager implementation in a managed package that we install into all customer orgs. This ensures that all customers get the same version of the code and it can be controlled if we find any issues (for example, this known issue in Salesforce where queries on custom metadata don’t always respect the ORDER BY clause) or need to add new features. The only gotcha there is to remember that your trigger handler classes in the subscriber org need to be global.

I’ve extracted our package out to a Salesforce DX project that you can download here: