Apex triggers come in two timing categories (before and after) running against five DML events (insert, update, delete, undelete, and the after merge variant). That gives you the seven canonical trigger contexts every Salesforce developer is expected to know:
| Context | When it fires | Typical use |
|---|---|---|
before insert | After validation, before the row hits the database | Set defaults, derive fields, validate values |
before update | Before changes are persisted | Same as above + reject changes via addError |
before delete | Before the row is removed | Block deletion if related records exist |
after insert | Row now has an Id | Create related child records, fire callouts (via async) |
after update | Updated row is persisted, old values still accessible via Trigger.old | Roll up to parents, audit, notify |
after delete | Row is gone, accessible via Trigger.old | Cleanup of dependent rows, audit trail |
after undelete | Restored from Recycle Bin | Re-establish derived state |
Before triggers — fast, in-place, no extra DML
trigger AccountBefore on Account (before insert, before update) {
for (Account a : Trigger.new) {
if (a.Industry == null) a.Industry = 'Unspecified';
if (a.Rating == null && a.AnnualRevenue >= 10_000_000) a.Rating = 'Hot';
}
}
Notice — no update statement at the bottom. In before context, Trigger.new is the records about to be written. Just mutate them; the platform persists what you leave behind. Doing an explicit update would cause a recursion error.
After triggers — read the world, react to it
trigger OpportunityAfter on Opportunity (after update) {
Set<Id> dirtyAccountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
Opportunity old = Trigger.oldMap.get(opp.Id);
if (opp.StageName != old.StageName && opp.IsWon) {
dirtyAccountIds.add(opp.AccountId);
}
}
if (!dirtyAccountIds.isEmpty()) {
AccountRollupService.recalcWonRevenue(dirtyAccountIds);
}
}
The pattern here is the same one you’ll write a hundred times: walk Trigger.new, compare to Trigger.oldMap, collect IDs to act on, then delegate.
The two pitfalls every interviewer probes
1. Trying to modify Trigger.new in an after trigger.
Records are read-only in after context. The reasoning: they’ve already been persisted, so the runtime doesn’t have a “draft” copy you can edit in place. You’d need to construct a new list, write to it, and update — which fires the trigger again and risks recursion.
2. Forgetting Trigger.oldMap is null in before insert.
New records don’t have IDs yet, so there’s no map keyed by Id. If you reach for Trigger.oldMap in before insert, you’ll get a NullPointerException. The right place to compare old vs new is before update or after update.
The “one trigger per object” rule
Salesforce doesn’t enforce trigger order. If you have three triggers on Account, they fire in arbitrary order, and that order can change between deployments. The community consensus — and Salesforce’s own architectural guidance — is one trigger file per object, delegating to a handler class:
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
new AccountTriggerHandler().run();
}
The handler decides which context-specific method to call based on Trigger.operationType. This gives you a single, ordered place to control execution and makes the trigger itself trivial.
Common interview follow-ups
- Why one trigger per object? — Predictable order, single test target, easier to add recursion guards and skip flags.
- Can you call a trigger from another trigger? — Indirectly, yes — any DML inside a trigger fires the same-object trigger again. That’s why every framework has a recursion guard.
- Which context for which problem? — Default to
beforewhen you’re mutating the record itself; default toafterwhen you’re touching anything else.
Verified against: Apex Developer Guide — Trigger Context Variables. Last reviewed 2026-05-17.