Apex Trigger Best Practices in Salesforce?

on

|

views

and

comments

Writing efficient code is very important in order to keep the code quality and performance high. When we develop high-quality code we also make sure that there is very less or no technical dept in the Salesforce Org.

Best Practices

As a developer, to develop effective triggers we can follow the below best practices

  1. One trigger per object
  2. Logic Less Trigger
  3. Context-specific handler methods
  4. Avoid SOQL Query inside for loop
  5. Avoid hardcoding IDs
  6. Avoid nested for loop
  7. Avoid DML inside for loop
  8. Bulkify Your Code
  9. Enforced Sharing in Salesforce
  10. Use @future Appropriately
  11. Use WHERE Clause in SOQL Query
  12. Use Test-Driven Development

1. One trigger per object

It’s always advised to create only one trigger per sObjectType and the benefit of having only one trigger is that you can

  1. Easy Maintainabilityย 
  2. Control the order of execution

If you create more than one trigger per object then you can not control which trigger logic will execute first.

Note: –

  1. If there are any managed package triggers then that can be considered as an exception.
  2. There is no such restriction that you can not create more than one trigger per object.

2. Logic Less Trigger

Before we talk about what logic-less apex triggers are let’s talk about a scenario here.

Scenario – When the contact record is created populate the description of the contact record with a static value “Login-Less Apex Triggers Rocks!

๐Ÿค” How you will solve it using Apex Trigger?

I have seen many developers write below code

				
					trigger ContactTrigger on Contact (before insert) {
    for(Contact con: Trigger.New){
        con.Description = 'Login-Less Apex Triggers Rocks!';
    }
}
				
			
  1. Will the above code work? – YES
  2. Is it an effective way of writing the code in triggers? – NO

As we are writing the logic inside the apex trigger it is not an effective way to write the trigger.

We should create a handler class and create a method inside that class and then write logic there.

Below is the modified version of the same problem.

Follow the below steps to modify the code

  1. Create an Apex Class
  2. Create a method inside the apex class
  3. Call the Apex Class method from Trigger
				
					public class ContactTriggerHandler {
    public static void handleBeforeInsert(List<Contact> contactList){
        for(Contact con: contactList){
            con.Description = 'Login-Less Apex Triggers Rocks!';
        }
    }
}
				
			
				
					trigger ContactTrigger on Contact (before insert) {
    ContactTriggerHandler.handleBeforeInsert(Trigger.New);
}
				
			

3. Context specific handler methods

We should always be using context-specific handler methods in the handler class. Let’s take an example here.

Requirement – Validate the contact address and then update the Address Status field with valid or invalid based on API Response.

In the above use case, You will after insert as we need contact record id to update the record.

So create a dedicated handler method for after insert and do the same for all other context.

below the modified apex trigger and apex class.

				
					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
    }
    
}
				
			
				
					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);
    }
    
}
				
			

4. Avoid SOQL Query inside for loop

As we all know that Salesforce works in Multi-Tenant Environment it is important that we keep all the limits in mind.

There is a SOQL Limit of 100 in every Transaction.

Requirement – When the contact is created, update the contact address same as the Account Address.

I have seen that there are developers who do write the code below. Where you see there is SOQL Query in Line no 7. If you insert 101 contact records at once you will get the SOQL error.

				
					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;
                // Add all other address fields
            }
        }
    }
    
    public static void handleAfterInsert(List<Contact> contactList){
        // validate the contact address using the API
    }
    
}
				
			

Instead of using the SOQL Query, we should be taking help from Salesforce Collection and then reducing the error.

Below is the modified version for the same.

				
					public class ContactTriggerHandler {
    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);
            }
        }
        Map<String, Account> accountMap = new Map<String, Account>();
        for(Account acc : [SELECT Id, BillingCity FROM ACCOUNT WHERE ID IN: accountIdsSet] ){
            accountMap.put(acc.Id, acc);
        }
        for(Contact con: contactList){
            con.Description = 'Login-Less Apex Triggers Rocks!';
            if(con.AccountId <> null){
                Account acc = accountMap.get(con.AccountId);
                con.MailingCity = acc.BillingCity;
                // Add all other address fields
            }
        }
    }
    public static void handleAfterInsert(List<Contact> contactList){
        // validate the contact address using the API
    }
    
}
				
			

5. Avoid hardcoding IDs

We should always avoid using hardcoding Ids in the Apex Class, Apex trigger.

For example, if you wanted to check if the account record type is a business account then only process the records.

Many developers do like the below code ๐Ÿ‘‡๐Ÿป

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

Instead of using the hardcoding Ids, we should always use Custom labels. Store the record type name there in the custom label and then use that custom Label in the apex code to query the record type.

See the below-modified code ๐Ÿ‘‡๐Ÿป

				
					public static void handleAccount(List<Account> accountList){
        String recordTypeId = Schema.SObjectType.Account.getRecordTypeInfosByName()
      						 .get(System.Label.BusinessAccountRecordTypeId)
      						 .getRecordTypeId();
        
        for(Account acc: accountList){
            if(acc.RecordTypeId == recordTypeId){
                // Process further Logic
            }
        }
    }
				
			

6. Avoid nested for loop

We should always try to avoid the nested for loop in the apex code which will impact the performance of our apex class.

Requirement – When the contact is created, update the contact address same as the Account Address.

				
					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
                    }
                }
            }
        }
    }
				
			

Instead of using nested for loops, we should use collection variables especially Map in apex class and get rid of for loops.

Below is the modified version of the above code.

				
					public class ContactTriggerHandler {
    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);
            }
        }
        Map<String, Account> accountMap = new Map<String, Account>();
        for(Account acc : [SELECT Id, BillingCity FROM ACCOUNT WHERE ID IN: accountIdsSet] ){
            accountMap.put(acc.Id, acc);
        }
        for(Contact con: contactList){
            con.Description = 'Login-Less Apex Triggers Rocks!';
            if(con.AccountId <> null){
                Account acc = accountMap.get(con.AccountId);
                con.MailingCity = acc.BillingCity;
                // Add all other address fields
            }
        }
    }
    public static void handleAfterInsert(List<Contact> contactList){
        // validate the contact address using the API
    }
    
}
				
			

7. Avoid DML inside for loop

Like we should avoid the SOQL statement inside for loop. In the similar fashion we should avoid making DML inside the for loop. We should use collection ( List ) to store all the records and then do the DML outside of for loop.

For Example, You need to create a child case when the Contact is created.

Below is the code which uses DML inside the for loop ๐Ÿ‘‡๐Ÿป

				
					public static void handleAfterInsert(List<Contact> contactList){
        // validate the contact address using the API
        List<Case> caseList = new List<Case>();
        for(Contact con : contactList){
            Case caseRecord = new Case();
            caseRecord.Subject = 'Sample Case';
            caseRecord.Origin = 'Email';
            caseRecord.ContactId = con.Id;
            caseRecord.Description = 'Sample Case';
            insert caseRecord;
        }
    }
				
			

If we use DML inside for loop then we will get into the governor limit of 151 DML. That means we can only make 150 DML in one transaction.ย 

Here is how we should use collection to avoid the DML inside for loop.

				
					public static void handleAfterInsert(List<Contact> contactList){
        // validate the contact address using the API
        List<Case> caseList = new List<Case>();
        for(Contact con : contactList){
            Case caseRecord = new Case();
            caseRecord.Subject = 'Sample Case';
            caseRecord.Origin = 'Email';
            caseRecord.ContactId = con.Id;
            caseRecord.Description = 'Sample Case';
            caseList.add(caseRecord);
        }
        insert caseList;
    }
				
			

8. Bulkify Your Code

As a developers, most of time we always write the code and test it for a single records.

So, if we only develop the code for a record then it will fail when there are bulk records.ย 

So we should always write the code which is bulkified and when writing the unit test we should always test it using min 200 records.

To write the bulkified code we should always use collections (List, Set & Map )

9. Enforced Sharing in Apex

By default, Apex Classes runs in System Mode which means it will never check Object or Field Level Security. No matter which user is running the class will always run in System Context.

To make sure that the classes are running in user context in order to make sure that our org is in compliance with security and also our code is secure from any threat.

Here is the link to learn more about how to developer secure apex in Salesforce.

10. Use @future Appropriately

Sometimes there are some scenarios where we want to run some logic in asynchronous mode or sometimes we get into some errors like Mixed DML operations.

We need to keep the @future or Queueable class and use it wherever we can use it.

Below are a couple of scenarios where we can use either @future or Queueable apex

  • We are getting mixed DML operation Error
  • We need to make a callout from Apex Trigger
  • The code does not needs to be part of the same transaction and can run in future

11. Use WHERE Clause in SOQL Query

I have noticed that as a developer we often forget to add the WHERE clause in the SOQL Query which results in unnecessary rows and also increases the SOQL Time.

So as a best practice we should always try to put the WHERE Clause in all the SOQL Queries and also the LIMIT Clause.

We also should query only those fields which are needed and remove any unnecessary fields from SOQL Query.

12. Use Test-Driven Development

Last but not least, Let’s try to use test-driven development approach which will let you know all the possible positive and negative scenarios of any requirement.

Test-driven development approach will be helpful to write code that is secure, and optimized, and will prevent the possibility of tech debt.

For more information visit the below links

Summary

These are some of the best practices I have tried to put together so that we all can grow together.

If you think that there are some important best practices please feel free to put in a comment and I will include those.

Happy learning ๐Ÿ˜Š

Amit Singh
Amit Singhhttps://www.pantherschools.com/
Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion
Share this

Leave a review

Excellent

SUBSCRIBE-US

Book a 1:1 Call

Must-read

How to start your AI Journey?

Table of Contents Introduction Are you tired of the same old world? Do you dream of painting landscapes with your code, composing symphonies with data, or...

The Secret Weapon: Prompt Engineering: ๐Ÿช„

Table of Contents Introduction Crafting the perfect prompt is like whispering secret instructions to your AI muse. But there's no one-size-fits-all approach! Exploring different types of...

How to Singup for your own Production org?

Let's see how we can have our salesforce enterprise Salesforce Org for a 30-day Trial and use it as a Production environment. With the...

Recent articles

More like this

LEAVE A REPLY

Please enter your comment!
Please enter your name here

5/5

Stuck in coding limbo?

Our courses unlock your tech potential