Design Patterns For Apex Triggers

Utilizing design patterns is essential to enhance code efficiency and prevent exceeding governor limits. Developers sometimes create inefficient code leading to repeated instantiation of objects, resulting in poor performance and a risk of breaching governor limits. This issue is particularly prevalent in triggers, especially when operating on a set of records.

This chapter explores key design pattern strategies to address these challenges.

Design Patterns for Bulk Triggers


In a real business scenario, it’s possible that you may encounter the need to process thousands of records simultaneously. If your trigger is not designed to handle such situations, it may encounter failures during record processing. To address this, there are best practices that should be adhered to when implementing triggers. All triggers are inherently bulk triggers and have the capacity to process multiple records concurrently. It is advisable to always plan for and accommodate the processing of more than one record at a time.

Let’s consider a business case where you need to handle a large number of records, and you’ve written the trigger as illustrated below. This is the same example we used for inserting an invoice record when the Customer Status changes from Inactive to Active.

// Bad Trigger Example
trigger Customer_After_Insert on APEX_Customer__c (after update) {
   
   for (APEX_Customer__c objCustomer: Trigger.new) {
      
      if (objCustomer.APEX_Customer_Status__c == 'Active' && 
         trigger.oldMap.get(objCustomer.id).APEX_Customer_Status__c == 'Inactive') {
         
         // condition to check the old value and new value
         APEX_Invoice__c objInvoice = new APEX_Invoice__c();
         objInvoice.APEX_Status__c = 'Pending';
         insert objInvoice;   //DML to insert the Invoice List in SFDC
      }
   }
}


You can observe that the DML statement is written inside the for-loop block, which functions well when processing only a few records. However, when dealing with hundreds of records, it may hit the governor limit for DML statements per transaction. We will delve into governor limits in detail in a subsequent chapter.

To circumvent this issue, it’s imperative to optimize the trigger for handling multiple records simultaneously.

The subsequent example will illustrate this concept:

// Modified Trigger Code-Bulk Trigger
trigger Customer_After_Insert on APEX_Customer__c (after update) {
   List<apex_invoice__c> InvoiceList = new List<apex_invoice__c>();
   
   for (APEX_Customer__c objCustomer: Trigger.new) {
      
      if (objCustomer.APEX_Customer_Status__c == 'Active' &&
         trigger.oldMap.get(objCustomer.id).APEX_Customer_Status__c == 'Inactive') {
         
         //condition to check the old value and new value
         APEX_Invoice__c objInvoice = new APEX_Invoice__c();
         objInvoice.APEX_Status__c = 'Pending';
         InvoiceList.add(objInvoice);//Adding records to List
      }
   }
   
   insert InvoiceList;
   // DML to insert the Invoice List in SFDC, this list contains the all records 
   // which need to be modified and will fire only one DML
}

This trigger will execute only one DML statement since it operates over a List containing all the records that require modification.

By employing this approach, you can circumvent the DML statement governor limits.

Helper Class for Triggers

Composing the entire code within a trigger is also considered poor practice. It is recommended to invoke an Apex class and delegate the processing from the trigger to the Apex class, as illustrated below. The Trigger Helper class is the class responsible for handling all processing related to the trigger.

Let’s revisit our example of creating an invoice record.

// Below is the Trigger without Helper class
trigger Customer_After_Insert on APEX_Customer__c (after update) {
   List<apex_invoice__c> InvoiceList = new List<apex_invoice__c>();
   
   for (APEX_Customer__c objCustomer: Trigger.new) {
      
      if (objCustomer.APEX_Customer_Status__c == 'Active' &&
         trigger.oldMap.get(objCustomer.id).APEX_Customer_Status__c == 'Inactive') {
         
         // condition to check the old value and new value
         APEX_Invoice__c objInvoice = new APEX_Invoice__c();
         objInvoice.APEX_Status__c = 'Pending';
         InvoiceList.add(objInvoice);
      }
   }
   
   insert InvoiceList; // DML to insert the Invoice List in SFDC
}

// Below is the trigger with helper class
// Trigger with Helper Class
trigger Customer_After_Insert on APEX_Customer__c (after update) {
   CustomerTriggerHelper.createInvoiceRecords(Trigger.new, trigger.oldMap);
   // Trigger calls the helper class and does not have any code in Trigger
}

Assisting Class

public class CustomerTriggerHelper {
   public static void createInvoiceRecords (List<apex_customer__c>
   
   customerList, Map<id, apex_customer__c> oldMapCustomer) {
      List<apex_invoice__c> InvoiceList = new Listvapex_invoice__c>();
      
      for (APEX_Customer__c objCustomer: customerList) {
         
         if (objCustomer.APEX_Customer_Status__c == 'Active' &&
            oldMapCustomer.get(objCustomer.id).APEX_Customer_Status__c == 'Inactive') {
            
            // condition to check the old value and new value
            APEX_Invoice__c objInvoice = new APEX_Invoice__c();
            
            // objInvoice.APEX_Status__c = 'Pending';
            InvoiceList.add(objInvoice);
         }
      }
      
      insert InvoiceList;  // DML to insert the Invoice List in SFDC
   }
}

In this approach, all processing has been offloaded to the helper class. When there’s a need for new functionality, we can effortlessly add the code to the helper class without having to modify the trigger.

One Trigger per sObject


It is advisable to create a single trigger for each object. Having multiple triggers on the same object can lead to conflicts and errors, especially if it reaches the governor limits.

To manage different scenarios, you can utilize context variables to invoke various methods from the helper class. Let’s revisit our previous example. If, for instance, the createInvoice method should only be called when the record is updated and on multiple events, you can control the execution as follows:

// Trigger with Context variable for controlling the calling flow
trigger Customer_After_Insert on APEX_Customer__c (after update, after insert) {
   
   if (trigger.isAfter && trigger.isUpdate) {
      // This condition will check for trigger events using isAfter and isUpdate
      // context variable
      CustomerTriggerHelper.createInvoiceRecords(Trigger.new);
      
      // Trigger calls the helper class and does not have any code in Trigger
      // and this will be called only when trigger ids after update
   }
}

// Helper Class
public class CustomerTriggerHelper {
   
   //Method To Create Invoice Records
   public static void createInvoiceRecords (List<apex_customer__c> customerList) {
      
      for (APEX_Customer__c objCustomer: customerList) {
         
         if (objCustomer.APEX_Customer_Status__c == 'Active' &&
            trigger.oldMap.get(objCustomer.id).APEX_Customer_Status__c == 'Inactive') {
            
            // condition to check the old value and new value
            APEX_Invoice__c objInvoice = new APEX_Invoice__c();
            objInvoice.APEX_Status__c = 'Pending';
            InvoiceList.add(objInvoice);
         }
      }
      
      insert InvoiceList; // DML to insert the Invoice List in SFDC
   }
}