Salesforce Trigger Framework Best Practices: Bulk-Safe, Secure, and Scalable Apex
A simple, reusable pattern for one-trigger-per-object, bulk-safe logic, recursion control, and built-in security.
Preview
Triggers are powerful but dangerous if written poorly.
This guide walks through a lightweight Trigger Framework you can use from day one:
- One trigger per object
- Logic-less triggers with handlers
- Bulkification patterns
- Recursion guards
- CRUD/FLS/sharing enforcement
Why a Trigger Framework?
Without structure, triggers quickly become messy: multiple triggers per object, un-bulkified code, random recursion bugs, and no security checks.
A Trigger Framework fixes this by:
✅ One trigger per object → predictable execution order
✅ Logic-less triggers → clean, testable code
✅ Bulk-safe design → no SOQL/DML in loops
✅ Recursion guards → no infinite loops or duplicate work
✅ Security-by-default → enforce CRUD/FLS/sharing consistently
Core Principles
- One trigger per object → All events consolidated in one trigger.
- Logic-less trigger → Trigger only routes to a handler class.
- Bulk-first design → Always think in Sets, Lists, Maps.
- Clear event routing → Methods like
beforeInsert,afterUpdate, etc. - Security by default → Enforce CRUD, FLS, and sharing.
- Recursion control → Prevent the trigger from re-firing endlessly.
Minimal Framework Structure
- Trigger: Thin, only routes execution.
- Handler: Class with event methods (before/after insert/update/delete/undelete).
- Service/DAO (optional): Shared logic for queries/DML with security checks.
Example: one trigger per object
- Keeps the trigger lightweight and deterministic by delegating all event paths to a handler.
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
AccountTriggerHandler handler = new AccountTriggerHandler();
if (Trigger.isBefore) {
if (Trigger.isInsert) handler.beforeInsert(Trigger.new);
if (Trigger.isUpdate) handler.beforeUpdate(Trigger.oldMap, Trigger.newMap);
if (Trigger.isDelete) handler.beforeDelete(Trigger.oldMap);
}
if (Trigger.isAfter) {
if (Trigger.isInsert) handler.afterInsert(Trigger.new);
if (Trigger.isUpdate) handler.afterUpdate(Trigger.oldMap, Trigger.newMap);
if (Trigger.isDelete) handler.afterDelete(Trigger.oldMap);
if (Trigger.isUndelete) handler.afterUndelete(Trigger.new);
}
}🔑 Notice how clean this is — no SOQL/DML, just routing.
Example: handler with recursion guard and selective DML
- Use a static guard to prevent re-entry and only perform updates when values actually change.
public with sharing class AccountTriggerHandler {
private static Boolean hasRun = false; // recursion guard
public void beforeInsert(List<Account> newAccounts) {
if (hasRun) return;
hasRun = true;
for (Account acc : newAccounts) {
if (String.isBlank(acc.Industry)) {
acc.Industry = 'Not Specified'; // default
}
}
}
public void beforeUpdate(Map<Id, Account> oldMap, Map<Id, Account> newMap) {
List<Account> toUpdate = new List<Account>();
for (Id accId : newMap.keySet()) {
Account oldAcc = oldMap.get(accId);
Account newAcc = newMap.get(accId);
if (oldAcc.Industry != newAcc.Industry) {
newAcc.Description =
'Industry changed from ' + oldAcc.Industry + ' to ' + newAcc.Industry;
toUpdate.add(newAcc);
}
}
if (!toUpdate.isEmpty()) update toUpdate;
}
// Add afterInsert, afterDelete, afterUndelete as needed
}Bulkification essentials
- Never put SOQL/DML in loops; collect IDs, query once with IN, and batch DML.
- Use Sets/Maps for O(1) lookups and deduplication.
- Prefer relationship queries and semi/anti-joins to avoid N+1 patterns.
Example: query once, map results
Set<Id> accountIds = new Map<Id, Account>(Trigger.new).keySet();
Map<Id, Contact> contactsByAcc = new Map<Id, Contact>(
[SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]
);Recursion control pattern
- Prevent infinite loops when triggers update the same object by tracking processed IDs or using a static Boolean per transaction.
public class RecursionGuard {
private static Set<Id> processed = new Set<Id>();
public static Boolean firstPass(Id recordId) {
if (processed.contains(recordId)) return false;
processed.add(recordId);
return true;
}
}
// Usage in handler:
if (!RecursionGuard.firstPass(acc.Id)) return;Security by Default
- Always use with sharing or inherited sharing.
- Enforce CRUD/FLS (Apex doesn’t do it automatically).
- Use
stripInaccessibleorWITH SECURITY_ENFORCED.
List<Account> safeAccounts =
(List<Account>) Security.stripInaccessible(
AccessType.READABLE,
[SELECT Id, Name, Industry FROM Account]
).getRecords();When to Use Flow vs Apex
✅ Flow → simple field updates (before-save Flows are very fast).
Get Mr. Akash’s stories in your inbox
Join Medium for free to get updates from this writer.
✅ Apex Triggers → complex cross-object logic, recursion, bulk processing, error handling.
Testing the Framework
- Test with 200 records to prove bulk-safety.
- Verify recursion guard prevents duplicate updates.
- Test with different profiles to confirm CRUD/FLS enforcement.
Do’s and Don’ts
✔ Do: one trigger per object
✔ Do: bulkify with Sets/Maps
✔ Do: enforce CRUD/FLS/sharing
✔ Do: add recursion guards
❌ Don’t: have multiple triggers on same object
❌ Don’t: put SOQL/DML in loops
❌ Don’t: rely on system context
Starter Checklist
- Create one trigger per object, delegate to handler.
- Implement clear methods per event.
- Add recursion guard + selective DML.
- Bake in CRUD/FLS/sharing enforcement.
- Write bulk (200) + security tests.
Closing
A small, opinionated Trigger Framework saves you from chaos.
Start with:
👉 One trigger per object
👉 A thin handler class
👉 Bulk + recursion guards
👉 Security by default
From there, evolve into services & DAOs as your org grows.
⚡Build once → Scale safely → Sleep peacefully 😴
Let’s Connect!👋
Thanks for reading! I love sharing Salesforce best practices, and I’d be happy to connect with fellow developers, admins, or anyone curious about Salesforce.
📱 Scan the QR Code below to connect with me directly (Gravatar):
Or just drop me a message — always happy to talk Salesforce, automation, and tech 🚀.









