Salesforce Apex Triggers are essential for automating business processes by executing custom logic before or after changes occur in Salesforce records. However, poorly written triggers can lead to performance issues, data inconsistencies, and frustrating debugging experiences. In this blog post, we’ll explore the best practices for optimizing Apex triggers to ensure your org remains scalable, efficient, and maintainable.
Every time a record is inserted, updated, or deleted, the platform executes any associated triggers. If these are not properly designed, it can cause:
That’s why following a consistent and optimized approach to trigger development is not optional—it’s crucial.
Avoid creating multiple triggers on the same object. Instead, create one trigger per object and manage logic execution through a dedicated handler class.
trigger ContactTrigger on Contact (before insert, before update, after insert) {ContactTriggerHandler.handleTrigger(Trigger.isBefore, Trigger.isAfter, Trigger.operationType, Trigger.new, Trigger.oldMap);}
This pattern improves maintainability and makes it easier to extend trigger logic without modifying the trigger itself.
Trigger handler frameworks are widely adopted to separate trigger logic from the trigger itself and to maintain consistency. Whether you roll your own or use a community-supported framework like Kevin O’Hara’s or FinancialForce’s, it provides structure and control.
public class ContactTriggerHandler {public static void handleTrigger(Boolean isBefore, Boolean isAfter, System.TriggerOperation operationType, List<Contact> newList, Map<Id, Contact> oldMap) {if (isBefore && operationType == System.TriggerOperation.BEFORE_INSERT) {beforeInsert(newList);}// Add more conditions as needed}private static void beforeInsert(List<Contact> contacts) {// Add business logic here}}
Never assume a trigger will only run on one record. Salesforce executes triggers in bulk by default (up to 200 records at a time). Therefore, all logic within the trigger must support bulk operations.
for (Account acc : Trigger.new) {Contact c = [SELECT Id FROM Contact WHERE AccountId = :acc.Id LIMIT 1];}
Set<Id> accountIds = new Set<Id>();for (Account acc : Trigger.new) {accountIds.add(acc.Id);}Map<Id, Contact> contactMap = new Map<Id, Contact>([SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]);
This reduces the number of SOQL queries to one and ensures compliance with governor limits.
A recursive trigger occurs when a DML operation within a trigger causes the same trigger to fire again. This can lead to infinite loops or reaching platform limits.
public class RecursionControl {public static Boolean isFirstRun = true;}
if (RecursionControl.isFirstRun) {RecursionControl.isFirstRun = false;// your logic here}
This pattern ensures the logic is only executed once per transaction.
Governor limits for SOQL queries (100 per transaction) and DML operations (150 per transaction) can be easily hit with poorly optimized triggers.
Map
and Set
collections to handle records in bulkSalesforce provides built-in Trigger context variables to help manage execution flow, such as:
Trigger.isInsert
, Trigger.isUpdate
, Trigger.isDelete
Trigger.isBefore
, Trigger.isAfter
Trigger.new
, Trigger.old
, Trigger.newMap
, Trigger.oldMap
Use these to tailor your logic based on when and how the trigger is fired.
if (Trigger.isUpdate) {for (Account acc : Trigger.new) {Account oldAcc = Trigger.oldMap.get(acc.Id);if (acc.Industry != oldAcc.Industry) {// Do something only if industry changed}}}
Triggers should fail gracefully and provide meaningful error messages for debugging.
if (someConditionFails) {throw new TriggerException('Custom validation failed.');}public class TriggerException extends Exception {}
Also consider logging errors to a custom object or using a tool like Platform Events or Slack integrations for critical alerts.
Proper testing is not just a best practice—it’s required for deployment.
@testSetup
methods to prepare dataSystem.assert()
@isTestprivate class ContactTriggerTest {@isTest static void testBeforeInsert() {List<Contact> contacts = new List<Contact>{new Contact(LastName='Test 1'),new Contact(LastName='Test 2')};insert contacts;// Add assertions hereSystem.assertEquals(2, [SELECT COUNT() FROM Contact WHERE LastName LIKE 'Test%']);}}
Optimizing Apex triggers is essential to maintain a healthy Salesforce org. By following these best practices—single trigger per object, bulkification, avoiding recursion, and efficient SOQL/DML usage—you can ensure your automation is scalable and future-proof.
Have questions or want to share your trigger tips? Drop a comment below or check out our full guide on Apex Trigger Context Variables!
Quick Links
Legal Stuff