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
- 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.
- 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. - Bulkify everything. Never SOQL or DML inside a
forloop. Triggers can receive up to 200 records per invocation, and Data Loader / API operations regularly do. - Recursion guard. A static boolean (or
Set<Id>) prevents the trigger from re-entering itself when its own DML re-fires it. - No hard-coded IDs. Use Custom Metadata, Custom Settings, or
Schema.describeSObjects()lookups — never paste an ID literal. - Honor sharing. Default the handler to
with sharingunless there’s a documented reason it must run as system. - 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.
- Use Custom Metadata for bypass flags. Production support sometimes needs to disable a trigger fast. A
Trigger_Settings__mdtrecord 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:
| Metric | Goal |
|---|---|
| Trigger transaction CPU | < 1,000 ms (10% of governor) |
| SOQL queries | ≤ 5 |
| DML statements | 1, ideally |
| Heap | < 1 MB |
| Callouts | 0 (do them async) |
Anti-patterns that fail code review
- Inline business logic in the trigger file
- Multiple triggers on one object
SELECTinside aforloopupdateorinsertinside aforloop- Skipping
Trigger.isBefore/isAfterchecks — running insert logic on update - Mutating
Trigger.newin anaftertrigger (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.