
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.cls3. 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';
}
}5. Use After Triggers for Related Records
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:
- Before triggers
- Validation rules
- Duplicate rules
- After triggers
- Assignment rules
- Auto-response
- Workflow rules
- Processes & flows
- Rollup summary
- After-save flows
- 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 Practice | Why Important |
|---|---|
| Bulkify everything | Avoid limits & errors |
| One trigger per object | Simpler architecture |
| Use handler class | Clean + maintainable |
| Before vs After usage | Correct logic |
| Avoid SOQL/DML in loops | Prevent CPU & limits |
| Prevent recursion | Prevent infinite loops |
| Leverage collections | Fast & scalable |
| Strong test coverage | Required 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.
