Skip to content

Trigger Handler

Piotr Kożuchowski edited this page Sep 28, 2021 · 2 revisions

Table of Contents

  1. Description
  2. Overview
  3. Basic Usage
  4. Multi-Tenant Environments

Description

Trigger Handler is apex design pattern which solves a few problems which arouse around apex triggers:

  1. If there's more than one trigger per SObject in org, order of trigger execution is not deterministic. Recommended practice is to have one trigger per SObject and delegate logic execution inside of it.
  2. If above is implemented without Trigger Handler framework, each trigger file would have repeated code for checking trigger operation, ex:
    switch on Trigger.operationType {
    when BEFORE_INSERT {
        new AccountContactLinker().linkContactsToAccount(Trigger.new);
        /*...*/
    }
    when BEFORE_UPDATE {
        /*...*/
    }
    when BEFORE_DELETE {
        /*...*/
    }
    when AFTER_INSERT {
        /*...*/
    }
    when AFTER_UPDATE {
        /*...*/
    }
    when AFTER_DELETE {
        /*...*/
    }
    when AFTER_UNDELETE {
        /*...*/
    }
}

Trigger Handler frameworks encapsulates this implementation and provide easy to extend virtual class with onBeforeInsert(List<SObject> newRecords) methods.



Overview

Trigger Handler

This virtual class is the heart of framework. It contains virtual methods which should be overwritten, each one corresponding to trigger event:

    public virtual void onBeforeInsert(List<SObject> triggerNew, TriggerContext tc) {}
public virtual void onAfterInsert(List<SObject> triggerNew, TriggerContext tc) {}

public virtual void onBeforeUpdate(List<SObject> triggerNew, TriggerContext tc) {}
public virtual void onAfterUpdate(List<SObject> triggerNew, TriggerContext tc) {}

public virtual void onBeforeDelete(List<SObject> triggerOld, TriggerContext tc) {}
public virtual void onAfterDelete(List<SObject> triggerOld, TriggerContext tc) {}

public virtual void onAfterUndelete(List<SObject> triggerNew, TriggerContext tc) {}

Concrete trigger handler (ex. AccountTriggerHandler) should extend this class and override methods it needs to handle and then delegate execution of logic to dedicated classes.

I believe TriggerHandler shouldn't have any other logic exception for delegation, since it violates Single Responsibility Principle. Record filtering for processing is semantically closer to the business class, than it is to the Trigger Handler (it's part of business requirement).

To prove it, we can interpolate solution to infinity and check if it's still maintainable. On one side, we will have a infinite number of one purpose classes which each filter records they need, plus unit tests. On other side, we have trigger handler with infinite number of filtering methods, plus service classes, plus tests for TriggerHandler filtering.
By comparison, we can see that it is easier to add, remove or edit code when TH only does delegation and it's easier to maintain it in VCS.




Trigger Dispatcher

Entry point of every trigger. This class encapsulates trigger context variables into TriggerContext instance and dispatches execution to correct Trigger Handler method. It may use Trigger Handler instance provided by developer or pull logic to run from custom metadata.

It contains 2 methods:

  • public static void runMetadataDefinedTriggers() which runs triggers defined in custom metadata.
  • public static void run(TriggerHandler triggerHandler) which runs concrete TriggerHandler class.

and 2 methods that are visible only in unit tests and provide ability to mock TriggerContext:

  • private static void runMetadataDefinedTriggers(TriggerContext triggerContext)
  • private static void run(TriggerHandler triggerHandler, TriggerContext triggerContext)

Trigger should contain only one line of code which executes trigger handler:

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


Trigger Context

This class serves following purposes:

  1. It encapsulates Trigger variables into an immutable object that can be passed down to other classes.
  2. It's used as marker interface which indicates that this particular method is run in Trigger context - similarly to SchedulableContext, QueueableContext and BatchableContext,
  3. It contains methods that make record filtering easier and more verbose:
public SObject[] getRecords()
public Map<Id, SObject> getRecordsMap()
public Set<Id> getRecordsIds()
public SObject getOld(SObject record)
public Map<Id, SObject> getOldMap()
public Boolean isNew()
public Boolean isChanged()
public Boolean isChanged(SObject record, SObjectField field)
public Boolean isChangedTo(SObject record, SObjectField field, Object toValue)
public Boolean isChangedFrom(SObject record, SObjectField field, Object fromValue)
public Boolean isChangedFromTo(SObject record, SObjectField field, Object fromValue, Object toValue)


Trigger Settings

Settings class for manipulating trigger execution and mocking in tests. Using this class, developer can turn off trigger execution for batch data fix for example.

  1. Toggling trigger execution for SObject type:
TriggerSettings.disableTrigger(Account.SObject);
// Do Something
TriggerSettings.enableTrigger(Account.SObject);

  1. Toggling all logic on custom setting level for current user. Methods below perform DML to update LogicSwitch__c custom setting for current user.
TriggerSettings.disableAllLogic();
TriggerSettings.enableAllLogic();
  1. Mocking custom metadata defined triggers:
TriggerSettings.mockMetadata(new List<TriggerLogic__mdt>{
    new TriggerLogic__mdt(Enabled__c = true, BeforeInsert__c = true, ApexClass__c = 'AccountContactLinker.cls')
});

//or mock whole selector class for more granular control

TriggerSettings.mockSelector(new CustomTriggerLogicSelector());


Interfaces

Framework provides 2 interfaces that should be implemented in business logic classes. It's not a requirement, but it streamlines the code and is a good practice.

TriggerHandler.Logic
    public interface Logic {
    void execute(List<SObject> records, TriggerContext ctx);
}

TriggerHandler.Logic represents single business requirement implementing class. TriggerContext marker interface indicates that this method runs in Trigger and may have to filter records for processing. TriggerContext contains methods that make filtering simpler and more verbose:

  List<Account> filtered = new List<Account>();

for (Account acc : (Account[]) records) {
    if (ctx.isNew() || ctx.isChanged(acc, Account.Email__c)) {
        filtered.add(acc);
    }
}


TriggerHandler.AsyncLogic
    public interface AsyncLogic {
    List<SObject> filter(List<SObject> records, TriggerContext ctx);
    void execute(List<SObject> records, QueueableContext ctx);
}

This interface marks classes which should execute asynchronously (using Queueable) on trigger event. Implementing classes does not have to implement Queueable interface, but may do that if needed.

It's similar to TriggerHandler.Logic, but it has additional method filter(List<SObject> records, TriggerContext ctx); which checks if Queueable should be queued. If method does not return any records, queueable is not scheduled.



TriggerHandler.Parameterizable
    public interface Parameterizable {
    void setParameters(String parameters);
}

This interface marks classes which can be parametrized through TriggerLogic__mdt.Parameters__c field. Value of the field will be passed to setParameters method.

This may come handy if we use Custom Metadata and want to parametrize the class - for example if we want to reuse one generic class for many sObjects types and pass different SObjectField as parameter, we can do that through this interface.

Exameple: Generic class that copies one field to another in trigger.

Custom Metadata: image

Code:

public with sharing class FieldCopier implements TriggerHandler.Logic, TriggerHandler.Parameterizable {
    private String sourceField, targetField;

    public void setParameters(String parameters) {
        String[] fields = parameters.split(',');
        this.sourceField = fields[0].trim();
        this.targetField = fields[1].trim();
    }

    public void execute(List<SObject> records, TriggerContext ctx) {
        for (SObject sobj : records) {
            sobj.put(targetField, sobj.get(sourceField));
        }
    }
}

Custom Metadata

Custom Settings

Basic Usage

If you are not working in Multi-Tenant environment, the easiest way to start is by implementing TriggerHandler

Multi-Tenant Environments

Defining logic in custom metadata
Parameterizing logic

Components

Type Name Description
Apex Class TriggerContext Encapsulation of Trigger variables for current run, Marker interface
Apex Class TriggerDispatcher Entry point to the trigger execution. Dispatches trigger execution to concrete TriggerHandler instance or custom metadata defined triggers
Apex Class TriggerDispatcherTest Unit Tests and examples for all trigger handler scenarios
Apex Class TriggerHandler Virtual class
Apex Class TriggerLogicSelector Selector class for querying and instantiating concrete classes of TriggerHandler.Logic defined in custom metadata
Apex Class TriggerSettings This class can be used to disable/enable trigger for specific SObject type or mock metadata defined logic
Custom Setting LogicSwitch__c Hierarchy custom setting which controls whether triggers / validation rules / flows should be ran for current user
Custom Object TriggerLogic__mdt Custom metadata used to define trigger logic to be run without concrete TriggerHandler. In Multi-Tenant environments, this approach decouples Trigger Handler dependency between teams/packages
Clone this wiki locally