Trigger Pattern for Tidy, Streamlined, Bulkified Triggers Revisited

I thought I’d launch my blog by revisiting a post I wrote for the Force.com Cookbook. You can find the original post here:

Trigger Pattern for Tidy, Streamlined, Bulkified Triggers

The main aims of this pattern are to organise trigger code and follow best practices with regard to handling bulk operations and keeping trigger logic together. I find that trigger code for an object is easier to understand and maintain when its one place and this pattern helps enforce that and avoids much of the head scratching that goes on when code in another trigger is the cause of the problem.

The pattern makes use of an interface to govern the order of events through a trigger in a logical way. Trigger logic is placed in a handler class that implements the interface. The interface makes it easy to cache data at the start of the trigger and perform actions against each object passed to the trigger and perform any post processing at the end. A factory class is responsible for initialising and executing the handler logic.

The main difference between this revision and the original post is the dynamic instantiation of the handler. In the previous version the handlers were hard coded into the factory class and in this version they are dictated by the trigger which specifies the handler to use. This is made possible by the System.Type class Salesforce added in the Summer 12 release. This approach has 2 advantages:

  • You don’t need to modify the factory class every time you want to create a new handler.
  • You have the option of creating multiple handlers for the same object. While this goes against the grain of keeping the trigger code in one place you may have a need to deploy some temporary trigger code or you don’t want to refactor an existing handler.

So onto the code …

This pattern involves delegating work from the trigger to a structured Trigger Handler class. Each object will generally have one trigger handler and the trigger will specify the handler to use. The trigger itself has almost no code in it. We make use of a Factory class to instantiate the appropriate trigger handler. Whenever we create a new trigger all we need to do is create a new handler class and add a line of code to the trigger itself to delegate the work to the trigger factory. The trigger factory takes care of instantiating the handler and calling the handler methods in the right order.

We start by defining an Interface that provides the template for our trigger handlers. This interface contains the method signatures each handler must implement. See below:

/**
 * Interface containing methods Trigger Handlers must implement to enforce best practice
 * and bulkification of triggers.
 */
public interface ITrigger
{
	/**
	 * bulkBefore
	 *
	 * This method is called prior to execution of a BEFORE trigger. Use this to cache
	 * any data required into maps prior execution of the trigger.
	 */
	void bulkBefore();

	/**
	 * bulkAfter
	 *
	 * This method is called prior to execution of an AFTER trigger. Use this to cache
	 * any data required into maps prior execution of the trigger.
	 */
	void bulkAfter();

	/**
	 * beforeInsert
	 *
	 * This method is called iteratively for each record to be inserted during a BEFORE
	 * trigger. Never execute any SOQL/SOSL etc in this and other iterative methods.
	 */
	void beforeInsert(SObject so);

	/**
	 * beforeUpdate
	 *
	 * This method is called iteratively for each record to be updated during a BEFORE
	 * trigger.
	 */
	void beforeUpdate(SObject oldSo, SObject so);

	/**
	 * beforeDelete
	 *
	 * This method is called iteratively for each record to be deleted during a BEFORE
	 * trigger.
	 */
	void beforeDelete(SObject so);

	/**
	 * afterInsert
	 *
	 * This method is called iteratively for each record inserted during an AFTER
	 * trigger. Always put field validation in the 'After' methods in case another trigger
	 * has modified any values. The record is 'read only' by this point.
	 */
	void afterInsert(SObject so);

	/**
	 * afterUpdate
	 *
	 * This method is called iteratively for each record updated during an AFTER
	 * trigger.
	 */
	void afterUpdate(SObject oldSo, SObject so);

	/**
	 * afterDelete
	 *
	 * This method is called iteratively for each record deleted during an AFTER
	 * trigger.
	 */
	void afterDelete(SObject so);

	/**
	 * andFinally
	 *
	 * This method is called once all records have been processed by the trigger. Use this
	 * method to accomplish any final operations such as creation or updates of other records.
	 */
	void andFinally();
}

We can now add a handler class that implements this interface. In the example below we have a handler for the Account object that has some logic to make sure the Account isn’t referenced elsewhere before it can be deleted. It also writes a record away to a custom object called Audit__c for each Account deleted. (Supporting classes and details of the Audit__c object will described at the end)

In the example we make use of the bulkBefore method to cache all the in use Account Id’s passed to the trigger in a Set. As this method is only called once we will not run the overhead of multiple SOQL queries. The validation is done in the beforeDelete method. This method is called iteratively for every record passed to the before delete trigger. If we were validating field data we would do this in one of the after methods since the values could be modified by another trigger or workflow. If the validation succeeds we add an Audit__c record to a list for handling later in the andFinally method. The andFinally method is executed once at the end of the trigger. In this case we use it to insert the Audit__c records.

You will also notice there is no SOQL in the class, this is delegated out to a Gateway class.

/**
 * Class AccountHandler
 *
 * Trigger Handler for the Account SObject. This class implements the ITrigger
 * interface to help ensure the trigger code is bulkified and all in one place.
 */
public without sharing class AccountHandler
	implements ITrigger
{
	// Member variable to hold the Id's of Accounts 'in use'
	private Set<Id> m_inUseIds = new Set<Id>();

	// Member variable to record Audit records
	private List<Audit__c> m_audits = new List<Audit__c>();

	// Constructor
	public AccountHandler()
	{
	}

	/**
	 * bulkBefore
	 *
	 * This method is called prior to execution of a BEFORE trigger. Use this to cache
	 * any data required into maps prior execution of the trigger.
	 */
	public void bulkBefore()
	{
		// If this a delete trigger Cache a list of Account Id's that are 'in use'
		if (Trigger.isDelete)
		{
			// pre load all the in use projects passed to this trigger
			m_inUseIds = AccountGateway.findAccountIdsInUse(Trigger.oldMap.keySet());
		}
	}

	public void bulkAfter()
	{
	}

	public void beforeInsert(SObject so)
	{
	}

	public void beforeUpdate(SObject oldSo, SObject so)
	{
	}

	/**
	 * beforeDelete
	 *
	 * This method is called iteratively for each record to be deleted during a BEFORE
	 * trigger.
	 */
	public void beforeDelete(SObject so)
	{
		// Cast the SObject to an Account
		Account myAccount = (Account)so;

		// Examine the Set and if the account is in use don't allow it to be deleted.
		if (m_inUseIds.contains(myAccount.Id))
		{
			// Add the error to the offending object
			so.addError('You cannot delete an Account that is in use.');
		}
		else
		{
			// Add an audit record to the list
			Audit__c myAudit = new Audit__c();
			myAudit.Description__c = 'Account Name: ' + myAccount.Name + ' (Id: ' + myAccount.Id + ') was deleted';

			m_audits.add(myAudit);
		}
	}

	public void afterInsert(SObject so)
	{
	}

	public void afterUpdate(SObject oldSo, SObject so)
	{
	}

	public void afterDelete(SObject so)
	{
	}

	/**
	 * andFinally
	 *
	 * This method is called once all records have been processed by the trigger. Use this
	 * method to accomplish any final operations such as creation or updates of other records.
	 */
	public void andFinally()
	{
		// insert any audit records
		if (!m_audits.isEmpty())
		{
			insert m_audits;
		}
	}
}

The factory class below instantiates the specified handler and executes the methods defined by the interface. It does this using the type of the handler class passed to the static method: createAndExecutHandler.

You will see from the execute method the order in which the interface methods are called and you will note the methods that are called iteratively passing in the relevant SObjects.

/**
 * Class TriggerFactory
 *
 * Used to instantiate and execute Trigger Handlers associated with sObjects.
 */
public with sharing class TriggerFactory
{
	/**
	 * Public static method to create and execute a trigger handler
	 *
	 * Arguments:	Type t - Type of handler to instatiate
	 *
	 * Throws a TriggerException if no handler has been found.
	 */
	public static void createAndExecuteHandler(Type t)
	{
		// Get a handler appropriate to the object being processed
		ITrigger handler = getHandler(t);
		
		// Make sure we have a handler registered, new handlers must be registered in the getHandler method.
		if (handler == null)
		{
			throw new TriggerException('No Trigger Handler found named: ' + t.getName());
		}
		
		// Execute the handler to fulfil the trigger
		execute(handler);
	}

	/**
	 * private static method to control the execution of the handler
	 *
	 * Arguments:	ITrigger handler - A Trigger Handler to execute
	 */
	private static void execute(ITrigger handler)
	{
		// Before Trigger
		if (Trigger.isBefore)
		{
			// Call the bulk before to handle any caching of data and enable bulkification
			handler.bulkBefore();

			// Iterate through the records to be deleted passing them to the handler.
			if (Trigger.isDelete)
			{
				for (SObject so : Trigger.old)
				{
					handler.beforeDelete(so);
				}
			}
			// Iterate through the records to be inserted passing them to the handler.
			else if (Trigger.isInsert)
			{
				for (SObject so : Trigger.new)
				{
					handler.beforeInsert(so);
				}
			}
			// Iterate through the records to be updated passing them to the handler.
			else if (Trigger.isUpdate)
			{
				for (SObject so : Trigger.old)
				{
					handler.beforeUpdate(so, Trigger.newMap.get(so.Id));
				}
			}
		}
		else
		{
			// Call the bulk after to handle any caching of data and enable bulkification
			handler.bulkAfter();

			// Iterate through the records deleted passing them to the handler.
			if (Trigger.isDelete)
			{
				for (SObject so : Trigger.old)
				{
					handler.afterDelete(so);
				}
			}
			// Iterate through the records inserted passing them to the handler.
			else if (Trigger.isInsert)
			{
				for (SObject so : Trigger.new)
				{
					handler.afterInsert(so);
				}
			}
			// Iterate through the records updated passing them to the handler.
			else if (Trigger.isUpdate)
			{
				for (SObject so : Trigger.old)
				{
					handler.afterUpdate(so, Trigger.newMap.get(so.Id));
				}
			}
		}

		// Perform any post processing
		handler.andFinally();
	}

	/**
	 * private static method to get the named handler.
	 *
	 * Arguments:	Type t - Class of handler to instatiate
	 *
	 * Returns:		ITrigger - A trigger handler if one exists or null.
	 */
	private static ITrigger getHandler(Type t)
	{
		// Instantiate the type
		Object o = t.newInstance();

		// if its not an instance of ITrigger return null
		if (!(o instanceOf ITrigger))
		{
		    return null;
		}

		return (ITrigger)o;
	}

	public class TriggerException extends Exception {}
}

Now we need to wire the trigger up to the TriggerFactory class. This is easily accomplished with a single line of code in the trigger below. The trigger simply passes the name of the handler to use to the factory method. You will notice that the trigger handles all CRUD operations. I havn’t included undelete but you could adapt the pattern to include this.

trigger AccountTrigger on Account (after delete, after insert, after update, before delete, before insert, before update)
{
	TriggerFactory.createAndExecuteHandler(AccountHandler.class);
}

The example requires the additional class below:

/**
 * Class AccountGateway
 *
 * Provides finder methods for accessing data in the Account object.
 */
public without sharing class AccountGateway
{
	/**
	 * Returns a subset of id's where there are any records in use.
	 *
	 * Arguments:	Set<Id> accIds - Set of Account Id's to examine
	 *
	 * Returns:		Set<Id> - Set of Account Id's that are 'in use'
	 */
	public static Set<Id> findAccountIdsInUse(Set<Id> accIds)
	{
		Set<Id> inUseIds = new Set<Id>();

		for (Account[] accounts : [Select p.Id, (Select Id From Opportunities Limit 1) From Account p where p.Id in : accIds])
		{
			for (Account acc : accounts)
			{
				if (acc.Opportunities.size() > 0)
				{
					inUseIds.add(acc.id);
				}
			}
		}

		return inUseIds;
	}
}

The Audit_c object is a simple custom object with an Autonumber Name field and a single text area field called Description__c.

A few things to note:

As with all triggers care should be taken to keep inside of governor limits and be aware that your trigger may be dealing with up to 200 records in a bulk operation.

  • Always add field validation to the after methods.
  • If you need data to persist across the before and after trigger consider the use of static variables.
  • Don’t put SOQL inside loops or any of the before and after methods.
Advertisements

Author, brainstormer, coder, dad, explorer, four chord trickster, gig goer, home worker, inquisitor, joker, knowledge seeker, likes: marmite, note scribbler, opinionator, poet, quite likes converse, roller skater, six music listener, tea drinker, urban dweller, vinyl spinner, word wrangler, x-factor hater, Yorkshireman (honorary), zombie slayer (lie).

Tagged with: , , ,
Posted in apex, code, force.com, salesforce
23 comments on “Trigger Pattern for Tidy, Streamlined, Bulkified Triggers Revisited
  1. Steve Cox says:

    Thanks for the great code, Tony. I’ve been using it for a couple years now. Here’s a further improvement, though: pass the Type around instead of typo-prone strings (i.e. createHandler(MyHandler.class)). Thanks again!

  2. Nice stuff. Are there similar design patterns that can be applied to the necessary tests for these triggers ?

  3. Al Hotchkiss says:

    Thank you for the great pattern. I have a one question. Is there any particular reason why you are using “without sharing” on the AccountHandler and AccountGateway classes?

    • Tony Scott says:

      Thanks Al. The with or without sharing is ultimately a decision for the developer. If the code was all in the trigger rather than delegated then it would execute as System so I wanted to keep in line with that in this example. In the handler I typically want access to all fields so I can do validation and in the example given if a user had access to the account but not opportunities they may be able to delete an account because the query may not return everything if it was running with sharing.

      • Allan Hotchkiss says:

        Thank you Tony. I wasn’t aware that code in a trigger runs as System. Your rationale for coding it the way you did makes a lot of sense. We are undergoing a security review and one of the warnings we are getting with the automated code scanner is that we shouldn’t use “without sharing” but I am guessing that it is not a hard and fast rule and that there are probably valid exceptions to it like the scenario you described.

  4. Janardhan says:

    Thank you tony.
    Am new to Apex coding, when i was running this code am getting errors like
    Error: Compile Error: Variable does not exist: AccountGateway at line 33 column 26 in AccountHandler class
    Error: Compile Error: Variable does not exist: id at line 25 column 34 in AccountGateway class
    and in trigger class AccountHandler.class not found
    please help how to get out this errors am new to this

    • Tony Scott says:

      Hi Janardhan, You will need to create the AccountGateway class first (see further down the example) to resolve the dependancy and avoid the compile error.

  5. Fred Hays says:

    This looks really good. However, would there be problems if you wanted any of the handler methods to be @future calls since you are not passing primitive types?

    • Tony Scott says:

      Hi Fred, apologies for the delay in replying. I’ve not been checking my blog for a while. It’s designed around iterating the trigger payload in a logical fashion and as such you would not want to make future callouts against each iteration. If you wanted to make future callouts you could consider using the andFinally() method to delegate to a future method. Regards, Tony

  6. Hi Tony,

    I’m new to OOPs and Apex, can you please advise the flow of this design pattern. If I want to implement this for Opportunity object and I need to write this for after Insert event, then where I have to register my new handler class? I’m very confused.

    Thanks

    • Tony Scott says:

      Hi Bennie, You register the handler inside your trigger code. In the example: TriggerFactory.createAndExecuteHandler(AccountHandler.class); AccountHandler is the class that the TriggerFactory class will delegate to. As for the flow, if you follow through the execute method of the TriggerFactory class, you should see clearer what’s going on. Hope this helps point you in the right direction.

      • Thanks, my doubt got cleared. I have registered the new lines of codes inside each handler class inside the AfterInsert method. It works fine if I update the related records inside AfterInsert() method itself, If I do the DML inside the AndFinally() method then it throws an exception. Can you please advise why ?

        Also, the biggest advantage of this pattern is it automatically takes care of bulkification

      • Tony Scott says:

        The AndFinally will get called in all contexts whether you are in the before or after (assuming your trigger has both before and after coded). So you would need to check the context, e.g. if (Trigger.isAfter && Trigger.isInsert)

  7. Akshay Dhiman says:

    Can you guys please post some more example using this pattern. Like using all of the events(before and after)?
    I’m new in this so that can help me a lot. Thanks in advance.

  8. Hi Tony, if I have to update related records based on the status__c of the current chunk of records, approach (1) can I go directly to AfterInsert method and collect the ids and then process it using a helper class in AndFinally method or (2) collect up in bulkAfter and then process it using a helper class in AndFinally ?

    • Tony Scott says:

      Hi Bennie, I would approach 1 since the pattern is already iterating the trigger payload for you. It will save having another loop in the AndFinally method.

  9. Hi Tony, I have implemented your tidy pattern in my org for few sObjects. As the trigger will have only one line of code whenever I update or insert my sObject the code coverage should be 100% but it’s zero for each trigger. Can you please advise why?

    • Tony Scott says:

      Hi Bennie, Are you swallowing any exceptions in your unit tests? If an insert or an update fails then you won’t achieve the code coverage. It’s worth checking debug logs to make sure the insert or update is succeeding.

  10. Wen HAo says:

    Hi Tony,

    Thanks for the effort to come out with the pattern but there are still some doubts to clarify before i can implement into my project. My case object are shared across multiple projects, in this case i’ll need to create multiple caseHandlers to segregate the biz rules to avoid the confusion. How can i dispatch the case trigger to the correct handler? Thx!

    • Tony Scott says:

      The handler is designed to encapsulate all your business logic rather than segregate it. You could conditionally delegate out to a helper class from any of the implemented interface methods to segregate your business logic if you wanted to do it that way.

Comments are closed.

About Me
Product Services Developer at:
FinancialForce.com
All views expressed here are my own. More about me and contact details here.

Enter your email address to follow this blog and receive notifications of new posts by email.

Copyright (there isn’t any, feel free to reuse!)

CC0
To the extent possible under law, Tony Scott has waived all copyright and related or neighboring rights to MeltedWires.com Examples and Code Samples. This work is published from: United Kingdom.

%d bloggers like this: