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
- One trigger per object
- Logic Less Trigger
- Context-specific handler methods
- Avoid SOQL Query inside for loop
- Avoid hardcoding IDs
- Avoid nested for loop
- Avoid DML inside for loop
- Bulkify Your Code
- Enforced Sharing in Salesforce
- Use @future Appropriately
- Use WHERE Clause in SOQL Query
- 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
- Easy Maintainabilityย
- 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: –
- 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.
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!';
}
}
- Will the above code work? – YES
- 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
- Create an Apex Class
- Create a method inside the apex class
- Call the Apex Class method from Trigger
public class ContactTriggerHandler {
public static void handleBeforeInsert(List 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 contactList){
for(Contact con: contactList){
con.Description = 'Login-Less Apex Triggers Rocks!';
}
}
public static void handleAfterInsert(List 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 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 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 contactList){
Set accountIdsSet = new Set();
for(Contact con: contactList){
if(con.AccountId <> null){
accountIdsSet.add(con.AccountId);
}
}
Map accountMap = new Map();
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 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 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 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 contactList){
Set accountIdsSet = new Set();
for(Contact con: contactList){
if(con.AccountId <> null){
accountIdsSet.add(con.AccountId);
}
}
List 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 contactList){
Set accountIdsSet = new Set();
for(Contact con: contactList){
if(con.AccountId <> null){
accountIdsSet.add(con.AccountId);
}
}
Map accountMap = new Map();
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 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 contactList){
// validate the contact address using the API
List caseList = new List();
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 contactList){
// validate the contact address using the API
List caseList = new List();
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
- https://oktana.com/salesforce-tddd-test-driven-development/
- https://fredrikniklas.medium.com/applying-test-driven-development-tdd-on-salesforce-development-with-apex-part-1-bf4ee43e0518
- https://fredrikniklas.medium.com/applying-test-driven-development-tdd-on-salesforce-development-part-2-api-development-5d80d459f189
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 ๐