Recipes by Category

App Distribution (2) Bundle logic, interface and services for distribution. App Logic (37) The Apex programming language, workflow and formulas for logic. Collaboration (5) The Salesforce Chatter collaboration platform. Database (29) Data persistence, reporting and analytics. Integration (33) Web Service APIs and toolkits for integration. Security (9) Platform, application and data security. Tools (4) Force.com tooling User Interface (36) Visualforce MVC and metadata-drive user interfaces. Web Sites (12) Public web sites and apps with optional user registration and login.
Beta Feedback
Cookbook Home » Trigger Pattern for Tidy, Streamlined, Bulkified Triggers

Trigger Pattern for Tidy, Streamlined, Bulkified Triggers

Post by Tony Scott  (2013-04-17)

Status: Unverified
Level: novice

Practice Motivation

These patterns provide an elegant way of coding triggers to avoid bad practices such as repetitive SOQL queries that can hit governor limits and multiple triggers over the same object. The patterns enforce a logical sequence to the trigger code and in turn help to keep code tidy and more maintainable. Keeping trigger logic for each object in a single place avoids problems where multiple triggers are in contention with each other and makes it easier to debug. Enforcing up front caching of data within a trigger keeps the trigger 'bulkified' and avoids those nasty 'too many SOQL queries'.

Description

This pattern involves delegating work from the trigger to a structured Trigger Handler class. Each object will have it's own trigger handler. 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, add a few lines of code to the Factory class to register the correct handler and add a line of code to the trigger itself to delegate the work.

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 with 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 selects the correct handler and executes the methods defined by the interface. It does this by the Object type passed to the static method createHandler. Adding additional handlers requires extending the if statement in the getHandler method.

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:	Schema.sObjectType soType - Object type to process (SObject.sObjectType)
	 *
	 * Throws a TriggerException if no handler has been coded.
	 */
	public static void createHandler(Schema.sObjectType soType)
	{
		// Get a handler appropriate to the object being processed
		ITrigger handler = getHandler(soType);
		
		// 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 registered for Object Type: ' + soType);
		}
		
		// 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 appropriate handler for the object type.
	 * Modify this method to add any additional handlers.
	 *
	 * Arguments:	Schema.sObjectType soType - Object type tolocate (SObject.sObjectType)
	 *
	 * Returns:		ITrigger - A trigger handler if one exists or null.
	 */
	private static ITrigger getHandler(Schema.sObjectType soType)
	{
		if (soType == Account.sObjectType)
		{
			return new AccountHandler();
		}
		
		return null;
	}
}

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 object type 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.createHandler(Account.sObjectType);
}

The example requires the additional classes below:

public class TriggerException extends Exception {}
/**
 * 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.

Discussion

This trigger pattern should work in most situations. As with all triggers care should be taken.

  • 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.

Share

Recipe Activity - Please Log in to write a comment

I would like to know how one would write a test for this.  I see that the interface does not require code coverage but the trigger factory will not have full coverage as well as the Account will be missing coverage as well.  Can you give any suggestions?

by ccouture01242013  (2014-11-28)

Great Way to implement triggers

voted as verified by S.K.EZDHAN HUSSAIN  (2014-09-20)

I posted a slightly modified version of this on my blog that makes use of the System.Type class to allow the handler to be passed dynamically from the trigger. Check it out here along with some other useful patterns.

by Tony Scott  (2013-09-06)

This is a very good start for designing the trigger logic. I took this design along with Dan Appleman's trigger design (enhanced by Adam Purkiss work) and came up with more comprehensive trigger architecture framework. It is available as an unmanaged package and as a open source project in Google Code. Details about this new architecture framework can be found in my blog.

by Hari Krishnan  (2013-07-26)

nice code , thanks for sharing

by Meginfo.Bruce  (2011-12-15)

nice work,  this is very similar design that me and another coworker was designing,  One key difference was that instead of  the TriggerFactory/ITrigger we are considering an abstract class (TriggerHandlerBase)  that each objects handler would extend,  this allows to only override the methods that are needed instead of all methods required by the interface.  Do you see any pitfalls with this approach that we should be aware of,  we have not started to really bang on this pattern yet? 




by dutom  (2011-10-17)

This is a nice pattern. If the factory class is a factory, my preference would be to avoid calling execute() within the createTrigger method. The pattern actually "creates and executes", and the method might be better named as "handleTrigger" than "createTrigger". The difference is whether or not there is any need for a distinction between "create" and "execute", as in the case where you may want additional setup before firing off the handler.

For a special-purpose pattern like this, maybe it is not a big deal, but if there were any additional setup to perform on the ITrigger instance, it could make the result more configurable and more testable.

Thanks for posting the pattern. I can see many other benefits to using it as well.

by ken04302010  (2011-09-26)

great gob !!
but i think that handler classes need to be without sharing , 

by dudu avinoam  (2011-09-23)

I have worked with Tony previously and proof read the trigger pattern here before publication. It is an extremely elegant solution that allows simple testing as well as ensuring that most common pitfalls are avoided.

voted as verified by Paul Battisson  (2011-09-04)

I have worked with Tony and have implemented the suggested Trigger pattern and by far, this is the most elegant solution to handle trigger issues. We were able to avoid following:- 1. Repeatedly querying same data 2. Governor limit exceptions 3. Conflicting/ Self-calling code.

voted as verified by Ansh Verma  (2011-09-02)

X

Vote to Verify a Recipe

Verifying a recipe is a way to give feedback to others and broaden your own understanding of the capabilities on Force.com. When you verify a recipe, please make sure the code runs, and the functionality solves the articulated problem as expected.

Please make sure:
  • All the necessary pieces are mentioned
  • You have tested the recipe in practice
  • Have sent any suggestions for improvements to the author

Please Log in to verify a recipe

You have voted to verify this recipe.