Sitemap

Salesforce Trigger Framework Best Practices: Bulk-Safe, Secure, and Scalable Apex

4 min readSep 7, 2025

--

A simple, reusable pattern for one-trigger-per-object, bulk-safe logic, recursion control, and built-in security.

Press enter or click to view image in full size
Salesforce Trigger Framework

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

  1. One trigger per object → All events consolidated in one trigger.
  2. Logic-less trigger → Trigger only routes to a handler class.
  3. Bulk-first design → Always think in Sets, Lists, Maps.
  4. Clear event routing → Methods like beforeInsert, afterUpdate, etc.
  5. Security by default → Enforce CRUD, FLS, and sharing.
  6. 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 stripInaccessible or WITH 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):

Gravatar Profile (Scan to Connect)

Or just drop me a message — always happy to talk Salesforce, automation, and tech 🚀.

--

--

Mr. Akash
Mr. Akash

Salesforce dev on a mission—breaking flows, fixing bugs, automating life with n8n, cooking AI agents, and surviving Agentforce. Follow the chaos!🚀