Skip to main content

SF-0343 · Concept · Medium

What are triggers best practices?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

Salesforce trigger best practices have settled into a short, well-known list — the same list every senior developer and every Salesforce style guide repeats. They’re not opinions; ignoring them eventually causes a production outage.

The non-negotiables

  1. One trigger per object. Salesforce doesn’t guarantee execution order across multiple triggers on the same object. A single trigger file delegating to a handler gives you a single, predictable entry point.
  2. No business logic in the trigger file. The trigger should be one or two lines: instantiate the handler, call run(). Logic lives in classes that are testable on their own.
  3. Bulkify everything. Never SOQL or DML inside a for loop. Triggers can receive up to 200 records per invocation, and Data Loader / API operations regularly do.
  4. Recursion guard. A static boolean (or Set<Id>) prevents the trigger from re-entering itself when its own DML re-fires it.
  5. No hard-coded IDs. Use Custom Metadata, Custom Settings, or Schema.describeSObjects() lookups — never paste an ID literal.
  6. Honor sharing. Default the handler to with sharing unless there’s a documented reason it must run as system.
  7. Defer expensive work to async. Long callouts, multi-million-row scans, or anything over a few seconds belongs in Queueable or Batch, not the trigger transaction.
  8. Use Custom Metadata for bypass flags. Production support sometimes needs to disable a trigger fast. A Trigger_Settings__mdt record beats a code deploy.

A reference shape

trigger AccountTrigger on Account (
    before insert, before update, before delete,
    after insert, after update, after delete, after undelete
) {
    new AccountTriggerHandler().run();
}
public with sharing class AccountTriggerHandler {
    private static Boolean alreadyRan = false;

    public void run() {
        if (alreadyRan || !TriggerSettings.isEnabled('Account')) return;
        alreadyRan = true;

        switch on Trigger.operationType {
            when BEFORE_INSERT { /* ... */ }
            when AFTER_UPDATE { /* ... */ }
            // ...
        }
    }
}

What “fast” means for a trigger

A trigger runs inside the user’s save. Every millisecond shows up as page lag. Targets to keep in mind:

MetricGoal
Trigger transaction CPU< 1,000 ms (10% of governor)
SOQL queries≤ 5
DML statements1, ideally
Heap< 1 MB
Callouts0 (do them async)

Anti-patterns that fail code review

  • Inline business logic in the trigger file
  • Multiple triggers on one object
  • SELECT inside a for loop
  • update or insert inside a for loop
  • Skipping Trigger.isBefore/isAfter checks — running insert logic on update
  • Mutating Trigger.new in an after trigger (throws)
  • Throwing unhandled exceptions instead of addError
  • No recursion guard
  • No test coverage for the failure paths

Common interview follow-ups

  • Why one trigger per object? — Predictable order, single test target, single recursion guard, single bypass switch.
  • What’s the difference between a handler and a service class? — Handler dispatches based on trigger context. Service holds reusable business logic the handler delegates to. Many shops introduce both layers.
  • Do these rules apply in 2026 with Flow? — Yes, when Flow can’t do the job. The order is configuration → Flow → Apex, and once you reach Apex, these rules kick in.

Verified against: Apex Developer Guide — Triggers, Best Practices for Triggers. Last reviewed 2026-05-17.