Managing complex business logic in Salesforce can quickly become unwieldy, especially when dealing with multiple triggers and varied business requirements. We implemented a robust Apex Trigger Framework. This uses the Trigger Handler Pattern approach. It ensures scalability. It also ensures maintainability and reusability.
Our Apex Trigger Framework separates concerns by organizing logic into a trigger, a common handler, and specialized service classes. Below, we explain the framework and highlight its benefits.
The Approach
1. Trigger as the Entry Point The trigger serves as the entry point for handling DML events. These events include insert, update, delete, or undelete on a Salesforce object. It delegates the processing to a handler class, ensuring the trigger remains lean and focused on orchestration rather than logic.
2. Common Handler for Event Coordination The handler class acts as the intermediary between the trigger and the business logic. It maps the DML operations (e.g., BEFORE_INSERT, AFTER_UPDATE) to specific methods that encapsulate the logic for that operation. This coordination centralizes control and keeps the trigger code clean.
3. Service Classes for Business Logic Each business requirement is encapsulated in its own service class. These classes focus exclusively on implementing specific pieces of logic, such as validation or updates to related objects. By isolating concerns, service classes make the code easier to test, debug, and extend.
Scenario and Requirement
In a Salesforce environment, businesses often need to manage complex relationships between objects, such as Contacts and Accounts. In our case, we needed to update the Account record with the number of associated Contact records. This update occurs whenever a Contact is inserted, updated, or deleted. Additionally, we had to apply several validation checks to the Contact records. These checks ensure that the Birthdate and MobilePhone fields follow specific formats and rules.
Here’s a breakdown of the key requirements we needed to address:
- Field Validation:
We needed to apply validations to ensure that:- The Birthdate field is always populated.
- The MobilePhone field follows a specific format (exactly 10 digits).
- Track the number of Contacts per Account:
We need to update a custom field (Number_of_Contacts__c) whenever a Contact is added. This update should be made on the associated Account record. We also update this field when a contact is updated or deleted. This update reflects the new count of Contacts.
- Handle Complex Trigger Operations:
We had to ensure that the trigger could handle multiple operations. These operations include insert, update, delete, and undelete. We aimed to keep the logic clean and maintainable.
- Efficiently Handle Large Data Volumes:
As Salesforce orgs grow, triggers can run into issues with large data volumes. We needed a structure that would allow easy extension and optimization as our needs evolved.
How We Solved It Through the Framework
To solve this problem, we used an Apex Trigger Framework. It had a modular and extensible approach. This included a trigger, handler, and multiple service classes. Here’s how each part of the framework contributed to the solution:
1. Trigger:
The ContactTrigger was kept simple, and its primary job was to detect the operation type (insert, update, delete, undelete). It then delegated the actual business logic to the handler class based on the operation.
trigger ContactTrigger on Contact(
before insert, before update, before delete,
after insert, after update, after delete,
after undelete
) {
// Instantiate trigger handler in trigger
ContactTriggerHandler triggerHandler = new ContactTriggerHandler();
/* Determine the type of operation being performed and delegate
the logic to the appropriate handler method */
switch on Trigger.operationType {
when BEFORE_INSERT {
// Handle logic before inserting records
triggerHandler.handleBeforeInsert(Trigger.new);
}
when BEFORE_UPDATE {
// Handle logic before updating records
triggerHandler.handleBeforeUpdate(Trigger.new, Trigger.oldMap);
}
when BEFORE_DELETE {
// Handle logic before deleting records
triggerHandler.handleBeforeDelete(Trigger.old);
}
when AFTER_INSERT {
// Handle logic after records are inserted
triggerHandler.handleAfterInsert(Trigger.new);
}
when AFTER_UPDATE {
// Handle logic after records are updated
triggerHandler.handleAfterUpdate(Trigger.new, Trigger.oldMap);
}
when AFTER_DELETE {
// Handle logic after records are deleted
triggerHandler.handleAfterDelete(Trigger.old);
}
when AFTER_UNDELETE {
// Handle logic after records are undeleted
triggerHandler.handleAfterUnDelete(Trigger.new);
}
}
// Finally, call postProcess() to perform logic
triggerHandler.postProcess();
}
2. Handler Class:
The ContactTriggerHandler was responsible for directing the flow of the trigger logic. For each trigger operation (before or after insert, update, delete), it called the appropriate service methods. This modularized the logic and made it easy to add new operations without cluttering the trigger itself.
public without sharing class ContactTriggerHandler {
// Instantiate the service classes that contain the business logic for Contact handling.
public ContactAccountUpdateService objContactAccountUpdateService =
new ContactAccountUpdateService();
public ContactValidationService objContactValidationService =
new ContactValidationService();
/**
* Handles logic for the `before insert` event.
* Ensures validation checks are applied to new Contact records before they are inserted.
* @param triggerNew - List of new Contact records being inserted.
*/
public void handleBeforeInsert(List<Contact> triggerNew) {
for (Contact con: triggerNew) {
// Validate the Contact's birthdate before inserting.
objContactValidationService.conFilterBeforeInsertForBirthDate(con);
// Validate the Contact's mobile phone before inserting.
objContactValidationService.conFilterBeforeInsertForMobilePhone(con);
}
}
/**
* Handles logic for the `before update` event.
* Ensures validation checks are applied to updated Contact records.
* @param triggerNew - List of Contact records being updated.
* @param triggerOldMap - Map of Contact records' old values before the update.
*/
public void handleBeforeUpdate(List<Contact> triggerNew, Map<Id, Contact> triggerOldMap) {
for (Contact con: triggerNew) {
// Validate the Contact's birthdate during update.
objContactValidationService.conFilterBeforeUpdateForBirthDate(con, triggerOldMap);
// Validate the Contact's mobile phone during update.
objContactValidationService.conFilterBeforeUpdateForMobilePhone(con, triggerOldMap);
}
}
/**
* Handles logic for the `before delete` event.
* This method is currently empty but can include logic for validations or restrictions.
* @param triggerOld - List of Contact records being deleted.
*/
public void handleBeforeDelete(List<Contact> triggerOld) {
for (Contact con: triggerOld) {
// Placeholder for potential validations or restrictions during deletion.
}
}
/**
* Handles logic for the `after insert` event.
* Applies post-insertion logic such as related account updates.
* @param triggerNew - List of newly inserted Contact records.
*/
public void handleAfterInsert(List<Contact> triggerNew) {
for (Contact con: triggerNew) {
// Perform post-insertion updates to related accounts.
objContactAccountUpdateService.conFilterAfterInsert(con);
}
}
/**
* Handles logic for the `after update` event.
* Applies post-update logic such as related account updates.
* @param triggerNew - List of updated Contact records.
* @param triggerOldMap - Map of Contact records' old values before the update.
*/
public void handleAfterUpdate(List<Contact> triggerNew, Map<Id, Contact> triggerOldMap) {
for (Contact con: triggerNew) {
// Perform post-update updates to related accounts.
objContactAccountUpdateService.conFilterAfterUpdate(con, triggerOldMap);
}
}
/**
* Handles logic for the `after delete` event.
* Applies post-deletion logic such as updates to related accounts.
* @param triggerOld - List of deleted Contact records.
*/
public void handleAfterDelete(List<Contact> triggerOld) {
for (Contact con: triggerOld) {
// Perform post-deletion updates to related accounts.
objContactAccountUpdateService.conFilterAfterDelete(con);
}
}
/**
* Handles logic for the `after undelete` event.
* Applies post-undelete logic such as updates to related accounts.
* @param triggerNew - List of undeleted Contact records.
*/
public void handleAfterUnDelete(List<Contact> triggerNew) {
for (Contact con: triggerNew) {
// Perform post-undelete updates to related accounts.
objContactAccountUpdateService.conFilterAfterUnDelete(con);
}
}
/**
* Executes post-processing logic based on the current trigger operation.
* Calls appropriate methods in service classes to handle specific operations.
*/
public void postProcess() {
switch on Trigger.operationType {
when BEFORE_INSERT {
// Perform contact validation for birthdate and mobile phone before insert.
objContactValidationService.validateContactForBirthDate();
objContactValidationService.validateContactForMobilePhone();
}
when BEFORE_UPDATE {
// Perform contact validation for birthdate and mobile phone before update.
objContactValidationService.validateContactForBirthDate();
objContactValidationService.validateContactForMobilePhone();
}
when BEFORE_DELETE {
// Placeholder for additional logic before delete.
}
when AFTER_INSERT {
// Update related accounts after contact insert.
objContactAccountUpdateService.updateAccounts();
}
when AFTER_UPDATE {
// Update related accounts after contact update.
objContactAccountUpdateService.updateAccounts();
}
when AFTER_DELETE {
// Update related accounts after contact deletion.
objContactAccountUpdateService.updateAccounts();
}
when AFTER_UNDELETE {
// Update related accounts after contact undelete.
objContactAccountUpdateService.updateAccounts();
}
}
}
}
3.Service Classes:
We created two main service classes for our two requirements of validating contact details and rolling up the total number of contacts on account:
public with sharing class ContactValidationService {
/* Declare a list of contacts, which will be used
to validate birthdate */
public List<Contact> lstConFilterBirthDate = new List<Contact>();
/* Declare a list of contacts, which will be used
to validate mobile phone */
public List<Contact> lstConFilterMobilePhone = new List<Contact>();
/* Method to filter contacts before insert based on birthdate */
public void conFilterBeforeInsertForBirthDate (Contact con) {
if(con.Birthdate == null){
lstConFilterBirthDate.add(con);
}
}
/* Method to filter contacts before update based on birthdate */
public void conFilterBeforeUpdateForBirthDate(Contact con,
Map<Id, Contact> oldContactsMap) {
if(con.Birthdate == null){
lstConFilterBirthDate.add(con);
}
}
/* Method to filter contacts before insert based on mobile phone number */
public void conFilterBeforeInsertForMobilePhone (Contact con) {
if (con.MobilePhone != null) {
/* Regex to match exactly 10 digits */
if (!Pattern.matches('^\\d{10}$', con.MobilePhone)) {
lstConFilterMobilePhone.add(con);
}
}
}
/* Method to filter contacts before update based on mobile phone number */
public void conFilterBeforeUpdateForMobilePhone(Contact con,
Map<Id, Contact> oldContactsMap) {
if (con.MobilePhone != null) {
/* Regex to match exactly 10 digits */
if (!Pattern.matches('^\\d{10}$', con.MobilePhone)) {
lstConFilterMobilePhone.add(con);
}
}
}
/* Method to validate birthdate for all filtered contacts */
public void validateContactForBirthDate () {
for(Contact con : lstConFilterBirthDate) {
con.addError('Birthdate is required');
}
}
/* Method to validate mobile phone for all filtered contacts */
public void validateContactForMobilePhone () {
for(Contact con : lstConFilterMobilePhone) {
con.addError('Mobile Phone must be exactly 10 digits.');
}
}
}
- ContactAccountUpdateService: This class updates the Account record whenever a Contact is inserted, updated, deleted, or undeleted. It tracked the AccountId associated with each Contact and performed aggregate queries to count the number of Contacts per Account.
public with sharing class ContactAccountUpdateService {
/* Declare a set to store the Ids of the parent
object Account */
public Set<Id> accountIds = new Set<Id>();
/* Method to filter contacts and store account ids in after insert */
public void conFilterAfterInsert(Contact con) {
/* Check if the parent account Id is not null */
if (con.AccountId != null) {
/* Add the parent account Id of each new contact
record to the set of parent account Ids */
accountIds.add(con.AccountId);
}
}
/* Method to filter contacts and store account ids in after update */
public void conFilterAfterUpdate(Contact con,
Map<Id, Contact> oldContactsMap) {
/* Check if the parent Id is changed */
if (con.AccountId != oldContactsMap.get(con.Id).AccountId) {
/* Add the old parent Id to the set of parent Ids
to be updated, if applicable */
if (oldContactsMap.get(con.Id).AccountId != null) {
accountIds.add(oldContactsMap.get(con.Id).AccountId);
}
/* Add the new parent account Id to the set of parent
Ids, if applicable */
if (con.AccountId != null) {
accountIds.add(con.AccountId);
}
}
}
/* Method to filter contacts and store account ids in after delete */
public void conFilterAfterDelete(Contact con) {
/* Add the parent account Id of each deleted child
record to the set of parent Ids to be updated */
accountIds.add(con.AccountId);
}
/* Method to filter contacts and store account ids in after undelete */
public void conFilterAfterUnDelete(Contact con) {
/* Check if the parent account Id is not null */
if (con.AccountId != null) {
accountIds.add(con.AccountId);
}
}
/* Method to update the account records with the number of contacts */
public void updateAccounts() {
/* Declare a map to store the number of contacts
for each parent */
Map<Id, Integer> contactsCount = new Map<Id, Integer>();
/* Get the contact count for each account Id */
for (AggregateResult ar : [
SELECT AccountId, COUNT(Id) contactCount
FROM Contact
WHERE AccountId IN :accountIds
WITH SECURITY_ENFORCED
GROUP BY AccountId
]) {
/* Put the account Id and the contact count in
the map */
contactsCount.put((Id) ar.get('AccountId'),
(Integer) ar.get('contactCount'));
}
/* Create a list of parent records to update */
List<Account> parentsToUpdate = new List<Account>();
/* Loop through the parent records to update
the number of contacts field */
for (Account parent : [
SELECT Id, Number_of_Contacts__c
FROM Account
WHERE Id IN :accountIds
WITH SECURITY_ENFORCED
]) {
/* Check if the account Id is present in the map */
if (contactsCount.containsKey(parent.Id)) {
/* Update the number of contact field */
parent.Number_of_Contacts__c = contactsCount.get(parent.Id);
} else {
/* If not present, set the number of contacts
to 0 */
parent.Number_of_Contacts__c = 0;
}
/* Add the parent account to the list to update */
parentsToUpdate.add(parent);
}
/* Update the parent object */
if (!parentsToUpdate.isEmpty()) {
if (Schema.sObjectType.Account.isUpdateable()) {
update parentsToUpdate;
}
}
}
}
For instance, when a user creates or updates a Contact, the trigger passes the event to the handler. The handler delegates the validation logic to the ContactValidationService. It assigns the account update logic to the ContactAccountUpdateService. Each class operates independently but contributes to the cohesive handling of the event
Benefits of This Approach
- Separation of Concerns:
- Keeps the trigger lightweight and focused on orchestration.
- Isolates validation, processing, and related updates into distinct classes.
- Scalability:
- Adding new logic only requires creating or updating service classes without modifying the trigger or handler.
- Maintainability:
- The modular design simplifies debugging and future updates.
- You can individually test service classes, which makes it easier to identify and resolve issues..
- Reusability:
- You can reuse the logic encapsulated in service classes across different triggers or processes.
- Compliance with Best Practices:
- Enforces a single trigger per object and adheres to Salesforce’s recommendation to minimize logic in triggers.
Why Choose This Framework?
This framework is ideal for Salesforce implementations requiring flexibility and clarity in managing complex business logic. By separating orchestration, coordination, and logic, it lays a strong foundation for scalable and maintainable solutions in Salesforce development.


Leave a comment