Apex Triggers

Salesforce Apex Trigger Best Practices

Triggers enable you to perform actions, such as updating related records, sending notifications, or invoking external services, based on changes to Salesforce records.

In this article, we will show you some Salesforce Apex trigger best practices with examples. Some of the trigger scenarios that we are going to show you are quite basic, but they are not always applied correctly.

Apex Trigger Best Practices in Salesforce

1. One per sObject

Create an apex trigger for each object is a recommended best practices of apex trigger in Salesforce development. This approach involves designing and organizing your triggers in a way that ensures clarity, modularity, and maintainability. Let's explore why this practice is important and how to implement it effectively:

2. Trigger Handlers

A Trigger Handler Framework is an architectural pattern that helps you structure and manage your trigger logic in a modular and organized manner. It involves breaking down the logic of your triggers into separate classes, promoting better code organization, readability, and reusability.

Here is the syntax of trigger in Salesforce, a basic trigger example in which we can use a handler to manage the logic:

trigger AccountTrigger on Account (before insert) {
       
        if(Trigger.isBefore){
            if(Trigger.isInsert){
                for(Account acc : Trigger.new){
                    acc.Name += ' Trigger Renamed';
                }
            }
        }
    }
trigger AccountTrigger on Account (before insert) {
        if(Trigger.isBefore){
            if(Trigger.isInsert){
                // Handle all trigger logic on this method
                AccountTriggerHandler.renameAccountName(Trigger.new);
            }
        }
    }
    
    public class AccountTriggerHandler {
        public static void renameAccountName(List<Account> accs){
            
            // Here should be the trigger logic
            for(Account acc : accs){
                acc.Name += ' Trigger Renamed';
            }
        }
    }

3. Bulkify your Code

Bulkifying trigger in Salesforce refers to the practice of designing your triggers to handle multiple records in a single transaction, rather than processing records one by one. This ensures that your trigger can handle scenarios where multiple records are created, updated, or deleted simultaneously.

Salesforce processes records in batches to optimize performance. Without proper bulkification, triggers that process records one at a time can quickly consume CPU time, SOQL queries, and DML operations, leading to hitting governor limits and performance issues.

This is how to bulkify trigger example in Salesforce:

trigger AccountTrigger on Account (before insert) {
        if(Trigger.isBefore){
            if(Trigger.isInsert){
                for(Account acc : Trigger.new){
                    // Call the method per account n times 
                    // you should NOT execute SOQL and DML operations inside this method
                    AccountTriggerHandler.renameAccountName(acc);
                }    
            }
        }
    }
trigger AccountTrigger on Account (before insert) {
        if(Trigger.isBefore){
            if(Trigger.isInsert){
                // Handle the trigger logic on this method
                // You can use SOQL and DML here because you are not in a loop
                AccountTriggerHandler.renameAccountName(Trigger.new);
            }
        }
    }

4. Trigger only on Conditions and Use Maps

When performing bulk apex triggers, use collections like lists and maps to store and process records. Especially to avoid having two nested loops, this could hit governor limits. In this example, using Trigger.newMap and Trigger.oldMap you will avoid 1 loop.

If we're talking about an update triggers in apex, it is a good idea to add execution conditions so that the logic in the trigger apex class is only executed when it is necessary. This can help to improve the performance of your Salesforce application by preventing the trigger from being executed unnecessarily.

Here are some Salesforce apex trigger scenarios on how to use collections correctly:

trigger AccountTrigger on Account (before update) {
        if(Trigger.isBefore){
            if(Trigger.isUpdate){
                AccountTriggerHandler.renameAccountName(Trigger.new, Trigger.old);
            }
        }
    }
    
    public class AccountTriggerHandler {
        public static void renameAccountName(List<Account> accsNew, List<Account> accsOld){
            List<Account> accountsToUpdate = new List<Account>();
    
            // Filter the accounts to update
            for(Account newAcc : accsNew){
                for(Account oldAcc : accsOld){
                    if(newAcc.Id == oldAcc.Id && newAcc.Custom_Field__c != oldAcc.Custom_Field__c){
                        accountsToUpdate.add(newAcc);
                    }
                }
            }
            if(accountsToUpdate.size() == 0) return;
        }
    }
trigger AccountTrigger on Account (before update) {
        if(Trigger.isBefore){
            if(Trigger.isUpdate){
                AccountTriggerHandler.renameAccountName(Trigger.newMap, Trigger.oldMap);
            }
        }
    }
    
    public class AccountTriggerHandler {
        public static void renameAccountName(Map<Id, Account> accNewMap, Map<Id, Account> accOldMap){
            List<Account> accountsToUpdate = new List<Account>();
            
            // Filter the accounts to update
            for(Id accId : accNewMap.keySet()){
                if(accNewMap.get(accId).Custom_Field__c != accOldMap.get(accId).Custom_Field__c){
                    accountsToUpdate.add(accNewMap.get(accId));
                }
            }
            if(accountsToUpdate.size() == 0) return;
        }
    }

5. Avoid Hardcoding IDs

Avoiding hardcoding IDs means not directly using specific record IDs or constant values in your trigger logic. Instead, you should use more flexible and configurable methods to reference records, objects, or other constants.

public class AccountTriggerHandler {
        public static void renameAccountName(List<Account> accs){
            // This could change on a sandbox refresh or production deployment
            Id accRecordType = '012Dn000000FJIu'; 
        }
    }
public class AccountTriggerHandler {
        public static void renameAccountName(List<Account> accs){
            Id accRecordType = Schema.SObjectType.Account.getRecordTypeInfosByName().get('Standard').getRecordTypeId(); 
        }
    }

6. Selective SOQL Queries

Selective queries are designed to retrieve a specific subset of records from a database, utilizing indexes and filters to minimize the number of records scanned and improve query performance.

Don't forget to add conditions and limits to your queries, otherwise the database will retrieve a lot of unnecessary data.

Selective SOQL Queries in Apex code Salesforce

Why Selective Querying Matters:

Tips for Implementing Selective Querying:

Here is a part of a trigger code in Salesforce about how to perform queries correctly:

// Query all fields from Account, even fields that probably you won't use
    Account relatedAccount = [SELECT * FROM Account WHERE Id =: opp.AccountId];
// Query only fields to use and include a LIMIT is always a best practice as well
    List<Account> relatedAccounts = [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds LIMIT 1000];

7. Order of Execution

Salesforce trigger order of execution refers to the sequence in which various automation processes, such as triggers, workflows, processes, and validation rules, are executed when a record is created, updated, or deleted in Salesforce.

Each process has a specific role in the data manipulation process, and their interaction can affect the final state of the record and its related data.

Understanding the order of execution is essential for several reasons:

Here is the Salesforce execution order:

  1. Record-Triggered flows configured to run on before
  2. Before Triggers
  3. Validation rules
  4. Duplicate rules
  5. Record saved on the database
  6. After triggers
  7. Assigments rules
  8. Auto-Response rules
  9. Workflow rules
  10. Escalation rules
  11. Record-Triggered flows configured to run on after
  12. Updates Roll-up summary
  13. Commits all DML to the database (If nothing before failed, the record will be saved successfully)

8. Test Coverage

Testing triggers involves creating and running automated tests to verify the functionality and behavior of your triggers.

These tests simulate different scenarios that your triggers might encounter during actual usage to ensure they work as intended. Triggers and Trigger Handler class must have atleast 75% coverage to deploy into production.

Importance of Unit Tests

Tips to create Unit Tests

@isTest
    public class AccountTriggerTest {
        // Positive test: Verify that the trigger updates the custom field
        @isTest
        public static void testTriggerUpdateCustomFieldPositive() {
            // Create test Account and Opportunity records
            Account testAccount = new Account(Name = 'Test Account');
            insert testAccount;
            
            Opportunity testOpportunity = new Opportunity(Name = 'Test Opportunity', AccountId = testAccount.Id);
            insert testOpportunity;
            
            // Modify the Opportunity to trigger the update
            testOpportunity.StageName = 'Closed Won';
            update testOpportunity;
            
            // Retrieve the updated Account
            testAccount = [SELECT Id, Custom_Field__c FROM Account WHERE Id = :testAccount.Id];
            
            // Verify that the custom field was updated as expected
            Assert.areEqual('Closed Won', testAccount.Custom_Field__c, 'The Custom field should be on "Closed Won"');
        }
    
        // Negative test: Verify that the trigger doesn't updates the custom field
        @isTest
        public static void testTriggerUpdateCustomFieldNegative() {
            // Create test Account and Opportunity records
            Account testAccount = new Account(Name = 'Test Account');
            insert testAccount;
            
            Opportunity testOpportunity = new Opportunity(Name = 'Test Opportunity', AccountId = testAccount.Id);
            insert testOpportunity;
            
            // Modify the Opportunity should not trigger the update
            testOpportunity.StageName = 'Open';
            update testOpportunity;
            
            // Retrieve the updated Account
            testAccount = [SELECT Id, Custom_Field__c FROM Account WHERE Id = :testAccount.Id];
            
            // Verify that the custom field was not updated
            Assert.areNotEquals('Closed Won', testAccount.Custom_Field__c, 'The Custom field should not be "Closed Won"');
        }
    }

9. Error Handling

Error handling involves implementing mechanisms in your code to detect and manage unexpected situations, exceptions, or errors that can occur during the execution of your apex trigger class.

Importance of Proper Error Handling

Implementing Proper Error Handling

public class CaseTriggerHandler {
    
        public static void updateAccountHandler(List<Case> cases){
            List<Id> accountIdsToUpdate = new List<Id>();
            
            try {
                for (Case updatedCase : cases) {
                    if (updatedCase.Priority == 'High') {
                        accountIdsToUpdate.add(updatedCase.AccountId);
                    }
                }
                
                // Perform updates on related Account records
                List<Account> accountsToUpdate = [SELECT Id, Custom_Field__c FROM Account WHERE Id IN :accountIdsToUpdate];
                
                for (Account acc : accountsToUpdate) {
                    acc.Custom_Field__c = 'Updated for High Priority Case';
                }
                
                update accountsToUpdate;
            } catch (Exception e) {
                // Handle exceptions gracefully
                String errorMessage = 'An error occurred during trigger execution: ' + e.getMessage();
                
                // Log the error
                System.debug(errorMessage);
                
                // Roll back the transaction to prevent incomplete updates
                Database.rollback(sp);
                
                // Provide a meaningful error message to users or administrators
                cases[0].addError('An error occurred while processing this case. Please contact support.');
            }
        }
    }

10. Avoid Recursion

Recursion in triggers occurs when a trigger event, such as an update or insert, causes the same trigger or a related trigger to fire again. This can result in a loop of trigger executions that continues until it hits system limits or causes unexpected behavior.

Why Recursion Is a Concern

Preventing Recursion

In Apex, static variables retain their values between trigger executions. By using a static variable as a flag, you can keep track of whether the trigger has already been executed in the current transaction.

public class TriggerHandler {
        // Static flag to prevent recursion
        private static Boolean hasExecuted = false;
    
        public static void onBeforeUpdate(Map<Id, Account> newMapAcc, Map<Id, Account> oldMapAcc) {
            if (!hasExecuted) {
                hasExecuted = true;
                
                // Your trigger logic here
                
                // Reset the flag after trigger logic is executed to preventing the flag 
                // from blocking subsequent trigger events within the same transaction
                hasExecuted = false;
            }
        }
    }

11. Comments and Documentation

Comments are annotations within your code that provide explanations, notes, or context about the code's functionality. Documentation refers to a more comprehensive description of your code's purpose, behavior, and usage.

Importance of Comments and Documentation

12. Salesforce Governor Limits

Salesforce imposes resource usage limits on various operations and transactions to ensure fair usage and prevent monopolization of resources. These limits are known as governor limits.

Common Types of Governor Limits on Apex Triggers

While Apex triggers offer a potent way to streamline your Salesforce processes, it's essential to adhere to best practices to ensure efficient and maintainable code. With these basic trigger scenarios in Salesforce and following these best practices for apex triggers, you can harness the full potential of automation while maintaining code quality and system stability.