Salesforce Triggers Best Practises

Salesforce Triggers Best Practises
Chinmaya By Chinmaya
13 Min Read

Introduction

Salesforce triggers allows you to automate processes by executing Apex code before or after specific database events like Insert, Update, Delete, or Undelete.

While triggers are powerful, they can quickly become unmanageable if not written with best practises in mind.

Poorly designed triggers can lead to governor limit issues, performance bottlenecks, and unpredictable behaviour in your Salesforce org.

In this blog, we will cover the best practises for writing efficient and scalable Salesforce triggers to ensure your code is maintainable, performant and adheres to Salesforce governor limits.

Best Practises while writing Triggers

  1. One Trigger per object
  2. Logic less trigger (i.e use Trigger Handler Class)
  3. Avoid SOQL Query and DML statement inside FOR Loop.
  4. Bulkify your trigger code
  5. Avoid hard coding IDs and values.
  6. Use Context Variables Wisely
  7. Use Logic to prevent Recursion in Trigger.
  8. Test for Bulk Operations and Edge Cases
  9. Use AddError for validation
  10. Write meaningful error handling
  11. Use WHERE Clause in SOQL Query
  12. Write Unit Test for Trigger
  13. Enforced Sharing in Salesforce
  14. Use @future appropriately

1. One Trigger Per Object

When working with Salesforce triggers, it’s a best practice to have only one trigger per object.

While Salesforce allows you to create multiple triggers for the same object, it can quickly lead to confusion, unmanageable code, and unpredictable behaviour.

Why one trigger per object?

1. Order of Execution is Unpredictable

If you have multiple triggers on the same object, Salesforce does not guarantee the order in which they execute.

For example, if you have two triggers on the Account object—one that updates the Status field and another that sends an email—there’s no way to control which trigger runs first. If the email is sent before the status update, it could lead to sending incorrect information.

2. Easier to Maintain and Debug

Having a single trigger per object makes your code much easier to maintain. If you need to debug or update the trigger logic, you only have one place to look.

3. Better Performance

A single trigger that handles all operations (insert, update, delete, etc.) in a consolidated manner is generally more performant.

When multiple triggers are present, each trigger may have redundant logic or unnecessary database queries, which can negatively impact performance and even lead to hitting governor limits.

2. Logic less trigger (i.e use Trigger Handler Class)

One of the most important practices in writing Salesforce triggers is to avoid placing too much logic directly in the trigger itself.

Instead, delegate that logic to a separate Apex class, often called a Trigger Handler.

This separation ensures that your trigger remains clean and makes your code easier to maintain and test.

Example:

				
					trigger AccountTrigger on Account (before insert, before update) {
    AccountTriggerHandler. handleBeforeInsert(Trigger.new);
    AccountTriggerHandler. handleBeforeUpdate(Trigger.new);
}
				
			
				
					public class AccountTriggerHandler {
    public static void handleBeforeInsert (List<Account> newAccounts) {
        // Business logic for before insert
    }

    public static void handleBeforeUpdate (List<Account> updatedAccounts) {
        // Business logic for before update
    }
}
				
			

Why this is Important?

  1. Separation of concerns: Keeps the trigger simple and focused on invoking the necessary logic.
  2. Reusability: Logic can be reused by different triggers or called from different contexts.
  3. Testability: Easier to write unit tests for individual methods in the handler class.

3. Avoid SOQL Query and DML statement inside FOR Loop.

Why?
DML operations inside loops can hit governor limits.

Best Practice:
Collect records in a list and perform DML outside the loop.

Incorrect code:

				
					for (Account acc : Trigger.new) {
    update acc; // DML inside a loop
}
				
			

Correct Code:

				
					List<Account> accountsToUpdate = new List<Account>();
for (Account acc : Trigger.new) {
    acc.Name = 'Updated Name';
    accountsToUpdate.add(acc);
}
update accountsToUpdate; // Bulk DML
				
			

4. Bulkify Your Trigger Code

Why?

Salesforce processes records in batches of up to 200 records in a single transaction. If your trigger doesn’t handle bulk records, it may hit governor limits and fail.

Best Practice:

Use collections like List, Map, or Set to ensure your code works for multiple records at once. Never place SOQL queries or DML operations inside a loop.

Incorrect way of writing (SOQL in a loop):

				
					for (Account acc : Trigger.new) {
    Account existing = [SELECT Id FROM Account WHERE Name = :acc.Name];
}
				
			

Correct way of writing (Bulkified Query):

				
					Set<String> accountNames = new Set<String>();
for (Account acc : Trigger.new) {
    accountNames.add(acc.Name);
}

List<Account> existingAccounts = [SELECT Id, Name FROM Account WHERE Name IN :accountNames];
				
			

5. Avoid Hardcoding IDs or Values

Why?

Hardcoding record IDs or text values makes your trigger inflexible and difficult to manage, especially when deploying to different environments.

Best Practice:

Use Custom Settings, Custom Labels, or dynamically fetch data with SOQL.

Bad Example:

				
					if (acc.OwnerId == '005XXXXXXXXXXXX') {
    // Do something
}
				
			

Good Example:
Use a Custom Setting or query to get the Owner ID dynamically:

				
					Id ownerId = [SELECT Id FROM User WHERE Name = 'John Doe' LIMIT 1].Id;
if (acc.OwnerId == ownerId) {
    // Do something
}
				
			

6. Use Context Variables Wisely

Why?

Context variables like Trigger.new, Trigger.old, Trigger.isInsert, and Trigger.isUpdate provide information about the current trigger execution.

Best Practice:

Use these variables to control your trigger logic.

Example:

				
					if (Trigger.isInsert) {
    for (Account acc : Trigger.new) {
        acc.Name = 'New Account';
    }
}

if (Trigger.isUpdate) {
    for (Account acc : Trigger.new) {
        System.debug ('Old Name: ' + Trigger.oldMap.get (acc.Id).Name);
    }
}
				
			

7. Use Logic to prevent Recursion in Trigger

Why?

Triggers that perform DML operations can cause themselves to fire again, leading to infinite recursion and governor limit errors.

Best Practice:

Use a static variable in a helper class to prevent recursion.

Example:

				
					public class TriggerHelper {
    public static Boolean isTriggerExecuted = false;
}

trigger AccountTrigger on Account (before update) {
    if (!TriggerHelper.isTriggerExecuted) {
        TriggerHelper.isTriggerExecuted = true;
        for (Account acc : Trigger.new) {
            acc.Name = acc.Name + ' - Updated';
        }
    }
}
				
			

Here, we have used the static boolean variable (name = isTriggerExecuted) which ensures that the trigger logic runs only once.

8. Test for Bulk Operations and Edge Cases

Why?

Triggers must handle bulk records (200 at a time) and edge cases like null fields or unexpected data.

Best Practice:

Write test classes with scenarios for single, bulk, and edge-case testing.

Example Test Class:

				
					@isTest
public class AccountTriggerTest {
    @isTest static void testBulkInsert() {
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 200; i++) {
            accounts.add(new Account(Name = 'Test Account ' + i));
        }

        Test.startTest();
        insert accounts;
        Test.stopTest();

        System.assertEquals(200, [SELECT Count() FROM Account]);
    }
}
				
			

9. Use AddError for Validation

Why?

addError() allows you to prevent DML operations and display meaningful messages to users.

Best Practice:

Validate data and use addError() to throw user-friendly errors.

Example:

				
					for (Account acc : Trigger.new) {
    if (acc.Name == null) {
        acc.addError('Account Name cannot be blank.');
    }
}
				
			

10. Write Meaningful Error Handling

Why?

Error handling ensures your triggers behave predictably and provide helpful information during failures.

Best Practice:

Use try-catch blocks to log errors and handle exceptions.

Example:

				
					try {
    insert accounts;
} catch (DmlException e) {
    System.debug('Error: ' + e.getMessage());
}
				
			

11. Use WHERE Clause in SOQL query

Why?

Unoptimized SOQL can lead to governor limit errors.

Best Practice:

    • Use WHERE clauses to filter records.
    • Retrieve only the fields you need.
    • Use Maps to store query results for easy access.

Example:

				
					Map<Id, Account> accMap = new Map<Id, Account>(
    [SELECT Id, Name FROM Account WHERE Id IN :Trigger.newMap.keySet()]
);
				
			

12. Write Unit Test for Trigger

Why?

Unit tests ensure your triggers work as expected, handle edge cases, and meet Salesforce’s 75% code coverage requirement for deployment.

Best Practices:

    • Write test cases for single records, bulk records, and edge cases (e.g., null values or invalid data).
    • Use Test.startTest() and Test.stopTest() to simulate governor limits.
    • Include assertions to validate the expected behavior.

Example

				
					@isTest
public class AccountTriggerTest {
    @isTest static void testTrigger() {
        // Prepare test data
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 200; i++) {
            accounts.add(new Account(Name = 'Test Account ' + i));
        }

        Test.startTest();
        insert accounts; // This will invoke the trigger
        Test.stopTest();

        // Assertions
        System.assertEquals(200, [SELECT COUNT() FROM Account]);
    }
}
				
			

Key Points:

    • Test positive and negative scenarios.
    • Validate trigger logic in bulk (200 records).
    • Use System.assert() to verify expected outcomes.

13. Enforce Sharing Rules in Triggers

Why?

By default, Apex code runs in system context, ignoring object and field-level security. Enforcing sharing ensures that triggers respect user permissions and sharing rules.

Best Practice:

Use the “with sharing” keyword in Apex classes called from triggers to enforce security. This ensures your trigger logic respects record visibility.

Example:

				
					public with sharing class AccountTriggerHandler {
    public static void handleTrigger(List<Account> accounts) {
        // Logic here respects user sharing rules
        for (Account acc : accounts) {
            System.debug('Processing Account: ' + acc.Name);
        }
    }
}
				
			

Key Points:

    • Use “with sharing” for handler classes.
    • Avoid exposing sensitive data through triggers.
    • Always test your trigger logic under users with different permission sets.

14. Use @future Methods Appropriately

Why?

Triggers cannot make callouts to external systems directly or perform time-consuming processes synchronously. The @future method helps perform asynchronous operations like callouts or large processing tasks.

Best Practice:

    • Use @future(callout=true) for making external callouts.
    • Avoid overusing @future, as it adds asynchronous processing overhead.
    • Pass only required parameters to @future methods to avoid limits.

Example:

				
					trigger AccountTrigger on Account (after insert) {
    for (Account acc : Trigger.new) {
        if (acc.Name != null) {
            AccountHelper.processFutureCallout(acc.Id);
        }
    }
}

public class AccountHelper {
    @future(callout=true)
    public static void processFutureCallout(Id accountId) {
        // Callout logic to external systems
        System.debug('Processing account ID in @future: ' + accountId);
    }
}
				
			

Key Points:

    • Use @future only when required (e.g., callouts).
    • Do not invoke @future methods from another @future method, as it is not allowed.
    • Always handle governor limits for asynchronous processing.

Conclusion

Following these best practices ensures that your Apex triggers are efficient, scalable, and maintainable.

By avoiding common pitfalls like recursion, unbulkified code, and SOQL/DML in loops, you can deliver high-quality triggers that adhere to Salesforce governor limits.

Always test for bulk scenarios and edge cases to make sure your triggers work flawlessly in real-world conditions. 🚀

Share This Article
Follow:
Chinmaya is working as a Senior Consultant with a deep expertise in Salesforce. Holding multiple Salesforce certifications, he is dedicated to designing and implementing cutting-edge CRM solutions. As the creator of Writtee.com, Chinmaya shares his knowledge on educational and technological topics, helping others excel in Salesforce and related domains.
Leave a comment