Tip of the week # 217 : Apex Trigger Best Practices



Salesforce : Apex Trigger Best Practices

Description : Apex Triggers in Salesforce are a powerful way to extend platform functionality. They enable developers to perform custom actions before or after record changes such as insert, update, or delete operations. To maintain high code quality and performance, it is essential to write efficient and optimized trigger logic. While triggers provide a high degree of flexibility and control, following best practices is crucial to ensure that the code remains scalable, maintainable, and efficient.
Apex Trigger Best Practices :
  • One trigger per object


    We should always write one trigger per object, as having multiple triggers on the same object can cause unexpected behavior due to the lack of a guaranteed execution order. To avoid this, consolidate all trigger logic for an object into a single trigger and delegate the detailed processing to helper classes. This approach improves clarity, maintainability, and control over the execution flow.


    Note: –
    If there are any managed package triggers then that can be considered as an exception.
    There is no such restriction that you can not create more than one trigger per object.
  • Logic Less Trigger


    We should always write logic-less triggers to keep them simple, readable, and easy to maintain. Instead of placing business logic directly inside the trigger, delegate it to a dedicated helper class. By dividing the logic into well-structured methods, the code becomes cleaner, more reusable, and less complex.

    
        
    trigger OpportunityTrigger on Opportunity (before update, before insert, after insert, after update, after delete, after undelete) {
    
    	if(Trigger.isBefore && Trigger.isInsert) {
    		OpportunityTriggerController.validateOpportunity(Trigger.new)
    	}
    }
        
    	
    	
  • Context-specific handler methods


    We should always use context-specific handler methods in the trigger handler class. This means separating logic into methods that correspond to specific trigger events (e.g., beforeInsert, afterUpdate). Doing so keeps the code organized, easier to debug, and ensures each scenario is handled appropriately.

    
        
    trigger ContactTrigger on Contact (before insert, after insert) {
        if(Trigger.isBefore && Trigger.isInsert){
            ContactTriggerHandler.handleBeforeInsert(Trigger.New);
        }else if(Trigger.isAfter && Trigger.isInsert){
            ContactTriggerHandler.handleAfterInsert(Trigger.New);
        }
    }
    public class ContactTriggerHandler {
        public static void handleBeforeInsert(List<Contact> contactList){
            for(Contact con: contactList){
                con.Description = 'Login-Less Apex Triggers Rocks!';
            }
        }
        public static void handleAfterInsert(List<Contact> contactList){
            // validate the contact address using the API
        }
    }
        
    	
    	
  • Avoid SOQL Query inside for loop


    Salesforce operates in a multi-tenant environment, it is essential to design code with platform limits in mind to ensure fair resource usage and optimal performance. For example, there is a limit of 100 SOQL queries per transaction. Exceeding this limit results in a runtime exception, which can break the execution of your trigger or process. To avoid this, always use best practices such as bulkification, efficient query design, and proper use of collections.

    
        
    public class ContactTriggerHandler {
        public static void handleBeforeInsert(List<Contact> contactList){
            for(Contact con: contactList){
                con.Description = 'Login-Less Apex Triggers Rocks!';
                if(con.AccountId != null){
                    Account acc = [SELECT Id, BillingCity FROM ACCOUNT WHERE ID =: con.AccountId];
                    con.MailingCity = acc.BillingCity;
                }
            }
        }
    }
        
    	
    	
  • Avoid hardcoding IDs


    We should always avoid hardcoding IDs in Apex classes or triggers. Hardcoded IDs (such as record IDs, profile IDs, or record type IDs) can easily break when deploying to different environments (sandbox, staging, production) where those IDs may differ. Instead, use dynamic queries, custom settings, custom metadata, or the Schema methods to fetch IDs at runtime. This ensures better flexibility, reusability, and easier deployments.

    
        
    public static void handleAccount(List<Account> accountList){
        String recordTypeId = '0125e000000P2xSAMS';
        for(Account acc: accountList){
            if(acc.RecordTypeId == recordTypeId){
                // Process further Logic
            }
        }
    }
        
    	
    	
  • Avoid nested for query loop


    We should always avoid using nested for loops in Apex code, as they can significantly impact performance, especially when handling large data volumes. Instead, use efficient data structures such as maps, sets, or lists to reduce complexity and achieve the same result without excessive looping. This improves scalability and helps stay within Salesforce governor limits.

    
        
    public static void handleBeforeInsert(List<Contact> contactList){
        Set<String> accountIdsSet = new Set<String>();
        for(Contact con: contactList){
            if(con.AccountId != null){
               accountIdsSet.add(con.AccountId);
            }
        }
        List<Account> accountList = [SELECT Id, BillingCity FROM ACCOUNT WHERE ID IN: accountIdsSet];
        // Level 1 For Loop
        for(Contact con: contactList){
            con.Description = 'Login-Less Apex Triggers Rocks!';
            if(con.AccountId <> null){
                // Level 2 Foe Loop
                for(Account acc: accountList){
                    if(con.AccountId == acc.Id){
                        con.MailingCity = acc.BillingCity;
                        // Add all other address fields
                    }
                }
            }
        }
    }<br>
    Note : If you are using child query than it fine to use.
        
    	
    	
  • Avoid DML inside for loop


    We should always avoid placing SOQL queries or DML statements inside for loops. Doing so can quickly lead to governor limit exceptions and negatively impact performance. Instead, use collections (Lists, Sets, or Maps) to store the records during iteration, and then perform the required SOQL or DML operations outside the loop in bulk.

    
        
    public static void handleAfterInsert(List<Account> accounts){
        for(Account acc : accounts){
            Contact con = new Contact();
            caseRecord.Subject = 'Sample Case';
            con.LastName=acc.Name;
            insert con;
        }
    }
        
    	
    	
  • Use collections like List, Set and Map


    Always bulkify your code. When writing trigger logic, design it to handle multiple records at once instead of just a single record. Triggers in Salesforce can execute in bulk (up to 200 records at a time), and if your code isn’t bulkified, it may fail or hit governor limits when processing large data volumes. By bulkifying, you ensure your trigger is efficient, scalable, and safe from runtime exceptions.

    
        
    trigger AccountTrigger on Account (after update) {
    
    	Map<Id,String> mapOfIdByPhone = new Map<Id,String>();
    	for (Account acc : Trigger.new) {
    		Map<Id,Account> accOldMap = Trigger.oldMap;
    		if(acc.Phone != accOldMap.get(acc.Id).Phone) {
    			mapOfIdByPhone.put(acc.Id,acc.Phone);
    		}
        }
    
    	//Query-related Contacts for the updated Accounts
    	List<Contact> contacts = [SELECT Id, AccountId, Phone FROM Contact WHERE AccountId IN :mapOfIdByPhone.keySet()];
    
    	// Create a list to hold updated contacts
    	List<Contact> contactsToUpdate = new List<Contact>();
    
    	// Update the Phone field on Contacts
    	for (Contact con : contacts) {
    		con.Phone = mapOfIdByPhone.get(con.AccountId);
    		contactsToUpdate.add(con);
    	}
    
    	// Perform a single DML operation to update all contacts
    	if (!contactsToUpdate.isEmpty()) {
    		update contactsToUpdate;
    	}
    }
        
    	
    	
  • Bulkify Your Code


    Collections and maps are essential for writing efficient Apex code. They enable developers to handle large data volumes and perform operations in bulk, helping to stay within governor limits. Maps, in particular, are extremely valuable because they allow quick access to records by key, reducing the need for redundant SOQL queries and improving overall performance.

    
        
    public static void handleAfterInsert(List<Account> accounts){
    List<Contact> contacts=new List<Contact>();
        for(Account acc : accounts){
            Contact con = new Contact();
            caseRecord.Subject = 'Sample Case';
            con.LastName=acc.Name;
            contacts.add(con);
        }
        if(!contacts.isEmpty()){
        insert contacts;
        }
    }
        
    	
    	
  • Avoid recursion


    Recursion in triggers occurs when a trigger unintentionally calls itself repeatedly, usually because of record updates performed within the trigger. This can result in infinite loops, excessive processing, and governor limit exceptions. To prevent recursion, implement strategies such as using static variables, helper classes, or conditional checks to ensure the trigger logic executes only once per transaction when needed.

    
        
    trigger AccountTrigger on Account (after update) {
        AccountTriggerHandler.handleAfterUpdate(Trigger.new);
    }
    public class AccountTriggerHandler {
        // Static variable to track trigger execution
        private static Boolean isExecuted = false;
        
        public static void handleAfterUpdate(List<Account> accList) {
            // Prevent recursion
            if(isExecuted) {
                return;
            }
            isExecuted = true;
            
            List<Contact> contactsToUpdate = new List<Contact>();
            
            for(Account acc : accList) {
                if(acc.AnnualRevenue > 1000000) {
                    contactsToUpdate.add(new Contact(
                        Id = acc.Primary_Contact__c,
                        Description = 'High Value Account'
                    ));
                }
            }
            
            if(!contactsToUpdate.isEmpty()) {
                update contactsToUpdate;
            }
        }
    }
        
    	
    	
Note:
1. The maximum stack depth limit will reached when the trigger recursively calls itself and the limit is 16. So when the trigger calls itself more than that we got the Maximum Trigger depth Exceeded error.
2. We have maximum execution time for each Apex transaction is 10 minutes.
3. Salesforce limits the CPU usage consumed in a given transaction, which is approximately 10 seconds for Synchronous, and 60 seconds for Asynchronous Apex.
4. The heap size is dynamic memory allocation which is the memory that is stored while execution of the program. It will throw the error when the limit is reached which is 6MB for synchronous and 12 MB for Asynchronous.
5. The governor limit for SOQL is 100 queries per transaction, with a maximum of 50,000 records retrieved across those queries. These limits apply to the entire transaction, regardless of how many triggers or classes are executed.

Happy Sharing...

Everyone has their own favorites, so please feel free to share with yours contacts and comments below for any type of help!

Use below Chrome Extension for Productivity and share your feedback. Download Link : https://chromewebstore.google.com/detail/quick-shortcuts/ldodfklenhficdgllchmlcldbpeidifp?utm_source=ext_app_menu

Comments

Popular posts from this blog

User Data Privacy

Salesforce LWC : Compact Layout on Hover

Salesforce : Multi-Factor Authentication