The quick version: one trigger per object, no logic inside it, and a handler class that can be called directly from a test. Everything past that is preference. The frameworks people argue about solve the same three problems, and if your own approach solves those three, you do not need one.
The three problems
Order of execution. Two triggers on the same object run in an order Salesforce does not define. Not “an order you have to look up” — an order that is genuinely not guaranteed. So one trigger per object, and the handler decides what runs first.
Recursion. Your update fires a trigger, which updates something, which fires your trigger again. Without a guard you either hit the maximum trigger depth or, more often, do the same work several times and wonder why a field increments by three.
Testability. Logic inside a trigger body can only be tested by performing DML. Logic in a class can be tested by calling the class, which is faster and lets you test branches that are awkward to reach through the record.
The minimum that works
trigger AccountTrigger on Account (
before insert, before update, after insert, after update
) {
AccountTriggerHandler.run();
}
And the handler decides everything else from Trigger.operationType:
public with sharing class AccountTriggerHandler {
private static Boolean hasRun = false;
public static void run() {
if (hasRun) { return; }
hasRun = true;
switch on Trigger.operationType {
when BEFORE_INSERT { setDefaults(Trigger.new); }
when BEFORE_UPDATE { setDefaults(Trigger.new); }
when AFTER_UPDATE { syncRelated(Trigger.new, Trigger.oldMap); }
when else { }
}
}
}
That is a trigger framework. It is not fashionable, it has no interfaces, and it solves all three problems. The methods take the records as parameters, so a test can call setDefaults with a list it built in memory and never touch the database.
Where the static flag goes wrong
A blunt hasRun static blocks the second legitimate run as well as the recursive one. Two cases bite:
- Batch processing. Statics persist across the whole transaction, but each batch execute is a separate transaction, so this is usually fine. Usually is doing work in that sentence.
- Genuine second updates. A workflow field update or a related record’s trigger updating your object again is a real change you probably want to process.
The better guard tracks record IDs already processed for a given operation rather than a single boolean. It is more code, and it is the difference between “we suppress recursion” and “we suppress work”.
Bulkification is not optional and not a framework feature
No framework saves you from a SOQL query inside a for loop. A trigger can receive 200 records, and an update from Data Loader or the Bulk API will send exactly that. Query once into a Map, loop over the map, collect changes into a list, DML once after the loop. The limits this protects you from are in the governor limits reference, and they are per transaction, not per record.
When to reach for a published framework
A named framework earns its weight when you have several developers, many objects, and a need for consistent structure that survives people leaving. It gives you a shape everyone recognises and usually a way to disable handlers per user or per environment, which is genuinely useful during a data migration.
It does not earn its weight on an org with four triggers. The cost is real: a new developer now has to learn the framework before reading the logic, and a framework with a bug is harder to debug than a method with a bug.
Before and after, correctly
Set fields on the records in the trigger in before context, with no DML. Touch other records in after context, where IDs exist. Writing to Trigger.new in an after trigger throws, and doing a DML update on the same records you are triggering on is the recursion problem creating itself.
Frequently asked
Should new logic go in Flow instead? Often yes, for straightforward field updates and related record work. Apex wins for complex branching, callout orchestration, and anything needing real error handling. Mixing both on one object is where order-of-execution pain returns, so decide per object and write it down.
How many triggers per object? One. There is no second answer.
Does the framework need an interface? No. An interface helps when you have many handlers following the same shape. With three, it is ceremony.
Where do I put the test? Both places: unit tests calling handler methods directly for the branches, and at least one test that performs DML to prove the trigger is wired up at all.
