Apex Trigger Best Practices in Salesforce (2026 Guide + Examples)

Apex Trigger Best Practices — Complete Guide (With Examples)

Apex Triggers are one of the most powerful features in Salesforce development. But they are also one of the most misused features by beginners and even working developers.

A well-written trigger can make your system scalable.
A poorly written trigger can break governor limits, slow down the org, and cause endless bugs.

In this guide, we’ll cover all Salesforce Apex Trigger best practices with real examples that every developer MUST know.

What Is an Apex Trigger? (Simple Explanation)

An Apex Trigger runs automatically when Salesforce records are:

  • Inserted
  • Updated
  • Deleted
  • Undeleted

Think of it as an “automation engine for developers”.

It acts on two contexts:

  • Before Trigger → Modify data before saving
  • After Trigger → Work with related records, async jobs, callouts, etc.

1. Always Bulkify Your Trigger Logic

This is the #1 most important rule.

Salesforce executes triggers in bulk, meaning:

  • Single record
  • 200 records
  • 10,000 records (batch)

If your trigger only works for one record — it is a useless trigger.

Wrong (Non-Bulkified Code)

trigger ContactTrigger on Contact(before insert) {
    for(Contact con : Trigger.new){
        Account acc = [SELECT Name FROM Account WHERE Id = :con.AccountId];
        con.Custom_Field__c = acc.Name;
    }
}

⚠ Problems:

  • SOQL inside loop (governor limit violation)
  • Trigger fails for bulk inserts

Correct (Bulkified Code)

trigger ContactTrigger on Contact(before insert) {
    Set<Id> accIds = new Set<Id>();
    for(Contact con : Trigger.new){
        accIds.add(con.AccountId);
    }

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

    for(Contact con : Trigger.new){
        if(accMap.containsKey(con.AccountId)){
            con.Custom_Field__c = accMap.get(con.AccountId).Name;
        }
    }
}

2. Only One Trigger per Object

This is Salesforce’s most strongly recommended rule.

Why only one trigger per object?

  • No confusion
  • No unpredictable order of execution
  • Easy debugging
  • Clean trigger handler framework

Good structure:

AccountTrigger.trigger  
AccountTriggerHandler.cls

3. Use a Trigger Handler Class (Separation of Concerns)

DON’T write business logic inside your trigger.
ALWAYS move logic to a separate Apex class.

Trigger (Clean & Small):

trigger AccountTrigger on Account(before insert, before update, after insert) {
    AccountTriggerHandler.handle(Trigger.new, Trigger.old, Trigger.operationType);
}

Handler Class (Logic):

public class AccountTriggerHandler {

    public static void handle(List<Account> newList, List<Account> oldList, TriggerOperation op){
        
        if(op == TriggerOperation.BEFORE_INSERT){
            beforeInsert(newList);
        }
        if(op == TriggerOperation.BEFORE_UPDATE){
            beforeUpdate(newList, oldList);
        }
        if(op == TriggerOperation.AFTER_INSERT){
            afterInsert(newList);
        }
    }

    private static void beforeInsert(List<Account> accList){
        // your logic here
    }

    private static void beforeUpdate(List<Account> newList, List<Account> oldList){
        // your logic here
    }

    private static void afterInsert(List<Account> accList){
        // trigger-related logic
    }
}

4. Use Before Triggers for Validation & Field Updates

Use BEFORE Triggers when:

  • Setting field values
  • Validating data
  • Preventing DML

Example

beforeInsert(newList){
    for(Account acc : newList){
        acc.Name = acc.Name.trim();
        acc.Rating = 'Hot';
    }
}

Use AFTER Triggers when you need record Id.

  • Insert related records
  • Send emails
  • Publish platform events
  • Callouts (via @future / Queueable)

Example

afterInsert(accList){
    List<Contact> cons = new List<Contact>();
    for(Account acc : accList){
        cons.add(new Contact(LastName='Primary', AccountId=acc.Id));
    }
    insert cons;
}

6. Avoid SOQL / DML Inside Loops (VERY IMPORTANT)

Wrong :

for(Account acc : Trigger.new){
    insert new Contact(LastName='Test', AccountId=acc.Id);
}

Correct :

List<Contact> cons = new List<Contact>();

for(Account acc : Trigger.new){
    cons.add(new Contact(LastName='Test', AccountId=acc.Id));
}

insert cons;

7. Prevent Recursion (Most Common Developer Bug)

Triggers calling each other repeatedly = ORG DEAD.

Use a static boolean flag.

public class TriggerHelper {
    public static Boolean hasRun = false;
}
trigger AccountTrigger on Account(before update) {
    if(TriggerHelper.hasRun == false){
        TriggerHelper.hasRun = true;
        // business logic
    }
}

8. Always Consider Order of Execution

Salesforce triggers run in a specific order:

  1. Before triggers
  2. Validation rules
  3. Duplicate rules
  4. After triggers
  5. Assignment rules
  6. Auto-response
  7. Workflow rules
  8. Processes & flows
  9. Rollup summary
  10. After-save flows
  11. Async jobs

9. Use Maps for Fast Lookups

Maps are faster, cleaner, and best for large datasets.

  • Faster than list loops
  • Easy to handle records
  • Prevents governor limit issues

10. Write Test Class Focused on Bulk Scenarios

Don’t just test one record.

  • Test 200 records
  • Test recursion
  • Test governor limits
  • Test positive + negative

Final Best Practices Summary

Best PracticeWhy Important
Bulkify everythingAvoid limits & errors
One trigger per objectSimpler architecture
Use handler classClean + maintainable
Before vs After usageCorrect logic
Avoid SOQL/DML in loopsPrevent CPU & limits
Prevent recursionPrevent infinite loops
Leverage collectionsFast & scalable
Strong test coverageRequired for deployment

Conclusion

Apex Triggers are powerful, but only if written with best practices.
When you follow bulkification, trigger handler patterns, recursion control, and clean architecture — your Salesforce logic becomes scalable, fast, and reliable.

A well-written trigger = a happy org + a happy developer.

Share your love

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *