You need the discipline — one trigger per object, logic in classes, deterministic order, a bypass switch. Whether you need a framework depends on who touches your objects. Most teams are fully served by a ~40-line handler pattern. Metadata-driven frameworks earn their weight when admins, Flows, and multiple Apex teams share the same objects.
The problem triggers don't solve on their own
The platform lets you write as many triggers per object as you like, and then refuses to tell you what order they run in. Two before update triggers on Account execute in an order Salesforce explicitly documents as not guaranteed. That single fact is where most trigger pain starts: logic that works in the sandbox, reordered in production.
Add the other two structural gaps — there's no built-in way to switch a trigger off for a data migration, and nothing stops a trigger from re-firing itself through a workflow field update or a cascading DML — and you have the entire, complete case for trigger architecture. Ordering, bypass, recursion. Everything a framework legitimately does maps to one of those three.
Level 0: get the logic out of the trigger body
This isn't a framework; it's hygiene, and it's non-negotiable. Code in a trigger body can't be called from anywhere else — not from a test directly, not from a batch job that needs the same logic, not from anonymous Apex when you're debugging. One trigger per object, and the body does nothing but delegate:
trigger AccountTrigger on Account (
before insert, before update, after insert, after update
) {
AccountTriggerHandler.handle(
Trigger.operationType, Trigger.new, Trigger.oldMap
);
}With exactly one trigger per object, the ordering problem disappears by construction — the order is whatever your handler says it is, in plain Apex, reviewable in a pull request. Most of the value of every framework you've heard of is captured right here.
Level 1: a minimal handler — the whole thing
The next real need is usually bypass: a data migration is loading 400,000 records and you'd rather not run your enrichment logic 400,000 times. You don't need a package for this. A base class covers ordering, bypass, and dispatch in roughly forty lines:
public abstract class TriggerHandler {
private static Set<String> bypassed = new Set<String>();
public static void bypass(String handlerName) { bypassed.add(handlerName); }
public static void clearBypass(String handlerName) { bypassed.remove(handlerName); }
public void run() {
if (bypassed.contains(getName())) return;
switch on Trigger.operationType {
when BEFORE_INSERT { beforeInsert(); }
when BEFORE_UPDATE { beforeUpdate(); }
when AFTER_INSERT { afterInsert(); }
when AFTER_UPDATE { afterUpdate(); }
when else { }
}
}
protected virtual void beforeInsert() {}
protected virtual void beforeUpdate() {}
protected virtual void afterInsert() {}
protected virtual void afterUpdate() {}
private String getName() {
return String.valueOf(this).substringBefore(':');
}
}Kevin O'Hara's sfdc-trigger-framework is the canonical public version of this shape and is still a fine choice — it's small enough to read in one sitting, which is exactly the property you want. If your Apex team owns all the automation on your objects, this level is very likely where you should stop.
Recursion guards: use ID sets, not booleans
The classic recursion guard — a static Boolean hasRun flipped on first entry — is a bug waiting for a bulk job. Triggers process records in chunks of 200; a Data Loader run of 1,000 records invokes your trigger five times in one transaction, and a boolean guard silently skips chunks two through five. Track which records you've processed instead:
public class OpportunityRollups {
private static Set<Id> processed = new Set<Id>();
public static void recalc(List<Opportunity> opps) {
List<Opportunity> todo = new List<Opportunity>();
for (Opportunity opp : opps) {
if (processed.add(opp.Id)) { // add() returns false if already present
todo.add(opp);
}
}
if (todo.isEmpty()) return;
// ... real work on todo only
}
}Same static-variable mechanism, but correct under chunking, and it still stops the workflow-field-update re-entry that boolean guards were invented for.
Level 2: when metadata-driven frameworks earn their weight
Frameworks like Trigger Actions move the dispatch table out of Apex and into Custom Metadata: each piece of logic is its own class, and records define which classes run on which event, in which order, with per-action bypass. That's real machinery, and it has a real audience:
- Admins and developers share the objects. Flows and Apex can be sequenced in one ordered list instead of two automation systems racing each other.
- Multiple teams ship to one org. Teams add actions via metadata records instead of editing a shared handler class — fewer merge conflicts on the one file everyone touches.
- You package automation. A managed package can ship actions that subscriber orgs reorder or disable without code changes.
If none of those describe your org, the metadata layer is indirection you pay for on every debugging session — the answer to "what runs when an Account updates?" moves from one class into a query against metadata records. We covered composing cross-domain automation this way in Design Salesforce like a distributed system.
What no framework fixes
A framework routes code. It doesn't verify it. The worst trigger codebases we see in trials aren't the ones missing a framework — they're the ones where a perfectly nice handler pattern dispatches to logic nobody has tested under bulk, under recursion, or under the order dependencies the framework supposedly manages. A framework with untested actions is a well-organized set of surprises.
The test that matters is the one people skip because it's slow to run against an org: insert 200 records, let every trigger fire, assert the end state. Locally, Nimbus fires your triggers on real DML in the proper order of execution — before triggers, save, after triggers — so that bulk test runs in milliseconds instead of a deploy cycle, and per-test governor tracking shows you exactly how many queries your trigger stack burns per chunk.
The actual answer
- Everyone: one trigger per object, zero logic in the body. This is level 0 and it's most of the value.
- Most teams: a minimal handler base class with bypass. Forty lines you own and can read.
- Recursion: static ID sets, never booleans.
- Metadata-driven frameworks: yes if admins, Flows, multiple teams, or packaging share your objects. Otherwise, no.
- Any level: bulk-test the logic, not the framework.
Test your trigger stack in milliseconds
Whatever framework you land on, the proof is a bulk test: 200 records in, every trigger fired, end state asserted. Nimbus runs that locally with no org and no deploy step.