Apex Triggers & the Handler Pattern

Trigger events, context variables, before vs after, one-trigger-per-object, recursion control and addError.

Developer 3 min readTriggersTrigger.newHandler patternRecursion

Syntax and events

apex
trigger AccountTrigger on Account (
    before insert, before update, before delete,
    after insert, after update, after delete, after undelete
) {
    AccountTriggerHandler.run();
}

Before vs after

Before triggerAfter trigger
Use forValidating / changing the same recordsWork needing the record Id or touching other records
Change fields on Trigger.new✅ no DML needed❌ read-only (FinalException)
Id available on insert❌✅
Typical exampleDefault a field, addError()Create child records, roll-ups, callouts (via async)

Context variables

VariableAvailable in
Trigger.newinsert, update, undelete
Trigger.oldupdate, delete
Trigger.newMapbefore update, after insert/update/undelete
Trigger.oldMapupdate, delete
Trigger.isBefore / isAfter / isInsert / isUpdate / isDelete / isUndeletealways
Trigger.operationTypeTriggerOperation.BEFORE_INSERT, …
Trigger.sizenumber of records in this chunk (≤ 200)

Detecting a change (update triggers)

apex
for (Opportunity opp : Trigger.new) {
    Opportunity old = Trigger.oldMap.get(opp.Id);
    if (opp.StageName != old.StageName && opp.StageName == 'Closed Won') {
        // react only to the transition
    }
}

Blocking records with addError

apex
trigger AccountTrigger on Account (before delete) {
    Set<Id> withOpenOpps = new Set<Id>();
    for (Opportunity o : [SELECT AccountId FROM Opportunity
                          WHERE IsClosed = false AND AccountId IN :Trigger.oldMap.keySet()]) {
        withOpenOpps.add(o.AccountId);
    }
    for (Account acc : Trigger.old) {
        if (withOpenOpps.contains(acc.Id)) {
            acc.addError('Cannot delete an account with open opportunities');
        }
    }
}

addError rejects just that record (or the whole DML when allOrNone = true) and shows the message in the UI.

The handler pattern (one trigger per object)

apex
trigger OpportunityTrigger on Opportunity (before insert, before update, after update) {
    new OpportunityTriggerHandler().run();
}

public with sharing class OpportunityTriggerHandler {
    public void run() {
        switch on Trigger.operationType {
            when BEFORE_INSERT { setDefaults((List<Opportunity>) Trigger.new); }
            when BEFORE_UPDATE { setDefaults((List<Opportunity>) Trigger.new); }
            when AFTER_UPDATE  { markCustomers((List<Opportunity>) Trigger.new,
                                               (Map<Id, Opportunity>) Trigger.oldMap); }
        }
    }
    private void setDefaults(List<Opportunity> opps) { /* … */ }
    private void markCustomers(List<Opportunity> opps, Map<Id, Opportunity> oldMap) { /* … */ }
}

Why: predictable order (multiple triggers on one object run in no guaranteed order), testable logic, easy to disable.

Recursion control

apex
public class TriggerGuard {
    private static Set<Id> processed = new Set<Id>();
    public static List<SObject> firstTime(List<SObject> recs) {
        List<SObject> out = new List<SObject>();
        for (SObject r : recs) if (processed.add(r.Id)) out.add(r);
        return out;
    }
}

A plain static Boolean flag also works but skips records in later 200-record chunks of the same transaction — a Set of processed Ids is safer.

Interview questions

  1. When do you use a before trigger instead of after?
  2. Why is Trigger.newMap null in before insert? → Records have no Id yet.
  3. How do you prevent a trigger from running twice? → Static Set/flag (recursion guard).
  4. Order of multiple triggers on the same object? → Not guaranteed — use one trigger per object.

Practice this