A trigger is easy to write and surprisingly easy to make fragile. A few lines that work for one record can become unpredictable when bulk data, Flow automation, multiple updates, managed packages, or asynchronous work enter the transaction. The goal of a trigger pattern is not to make Apex look elegant; it is to make side effects, execution context, and test behavior understandable.
For the Salesforce Platform Developer, a good trigger design starts by choosing the correct before/after context, keeps orchestration thin, processes collections in bulk, prevents accidental repeated work, and moves genuinely asynchronous responsibilities across an intentional transaction boundary.
Use before triggers to change the record being saved
A before trigger is the natural place to set or normalize fields on records that are already being inserted or updated. Because the records are still in the save process, the code can assign field values directly without issuing a second DML update on those same records.
Typical examples include deriving a classification, normalizing a business key, defaulting a value that depends on other fields, or enforcing a custom validation rule with addError(). Keeping these changes in before context avoids unnecessary DML and reduces the chance of recursive updates.
Do not confuse “before” with “do everything early.” Work that depends on a committed record ID, needs to create a relationship after the parent exists, or represents a side effect of the completed record change may belong in after context or in subsequent asynchronous processing.
Use after triggers for effects that depend on the saved record
After triggers run when the triggering records have been assigned system values associated with the save. They are useful for creating or updating related records, publishing follow-up work, or reacting to a state that must exist after the record has passed through the database save phase.
Fields on Trigger.new are not the place for direct assignment in after context. If business logic needs to change the triggering record again, that requires another DML operation, which can re-enter automation and create recursion. Ask whether the value could have been set in a before trigger instead.
After-trigger side effects should also be conditional on meaningful change. If a Case escalation job should run only when Status moves into an escalated state, compare old and new values rather than running the job on every update. Change detection is one of the simplest ways to make trigger behavior idempotent.
Keep one trigger thin and move behavior into handlers
A maintainable pattern is one unmanaged trigger per object with little or no business logic in the trigger body. The trigger routes the current context—before insert, after update, and so on—to handler methods or domain services that contain the testable logic.
This structure gives the team one obvious entry point for object-level Apex automation. Multiple independent triggers on the same object make execution order harder to reason about because developers should not build business correctness around an assumed ordering among separate triggers.
A handler pattern is not valuable because of a particular class name or framework. It is valuable when it separates orchestration from work, makes dependencies visible, supports bulk processing, and lets tests invoke behavior without constructing a maze of trigger-only branches.
Bulkify the handler, not just the trigger syntax
Every handler method should assume it may receive a collection. Gather identifiers from all relevant records, query related data once per logical need, organize results in maps, calculate outputs across the set, and perform collection DML after the loop.
Moving a SOQL statement from the trigger into a helper does not make it bulk-safe if the helper is still called once per record. Code review should focus on how many queries and DML statements execute as input size grows. The correct complexity is usually closer to “a few operations per transaction” than “one operation per record.”
Bulk design also affects error handling. If one record is invalid, decide whether the entire business operation should fail or whether partial success is appropriate. Do not let the choice be an accidental consequence of whichever DML method was easiest to type.
Prevent recursion with conditions and idempotency first
Recursion occurs when automation updates data that causes the same or related automation to execute again. A trigger that updates its own object in after update is a common example, but recursion can also involve Flow, workflow-style automation, rollups, or related objects that update each other.
The strongest prevention technique is to avoid unnecessary second updates. Use before context for same-record changes. In after context, compare old and new values and run work only when the relevant transition actually occurred. Design side effects so running the same logic twice does not create duplicate child records, duplicate notifications, or repeated integration messages.
Static state can be useful as a transaction-local coordination tool, but a single static Boolean such as isRunning is often too blunt. It can suppress legitimate later chunks or contexts in the same transaction and can hide a design that is not truly idempotent. If transaction-local tracking is required, a set of processed record IDs or operation keys is often more precise.
Respect the wider order of execution
Apex triggers are only one participant in record processing. Validation, before-save automation, triggers, assignment logic, after-save automation, rollups, and other platform behaviors can interact. A field observed early in the transaction may be changed later, and a subsequent update can cause portions of automation to run again.
Design around business invariants rather than assumptions such as “this trigger will be the last thing that touches the record.” If a calculation must represent the final state after several automation layers, determine which layer owns it and remove competing writers. Multiple mechanisms updating the same field are a maintenance problem even when governor limits are not yet failing.
Order-of-execution awareness is especially important when replacing legacy automation with Flow or when an org mixes managed-package triggers with custom Apex. Document who owns each side effect and which transitions are expected to re-enter automation.
Move slow or independent work to Queueable Apex
Some work does not belong in the synchronous save: callouts to external services, expensive enrichment, large follow-up processing, or tasks whose result is not required before the user receives confirmation. Queueable Apex is a strong default for discrete asynchronous jobs because it creates a separate execution context and supports structured inputs and chaining.
Asynchronous processing changes semantics. The record transaction can commit before the queued job runs, so downstream data is eventually consistent. The job may fail independently and needs monitoring or retry behavior. Do not enqueue work that the user must have completed before the save is considered successful.
Also avoid enqueuing one job per record from a bulk trigger. Aggregate work across the trigger set when possible, respect queueing limits, and pass identifiers or compact state rather than enormous object graphs.
Make trigger logic testable outside the trigger body
Thin triggers enable focused tests. Handler or service methods can be tested with known inputs, while integration-style Apex tests can verify that DML actually invokes the trigger and produces the expected database state. Both levels matter.
Tests should cover insert, update, delete or undelete contexts when the trigger supports them; bulk collections; meaningful field transitions; no-op updates; error paths; and any asynchronous work. For Queueable behavior, use the platform’s test controls to bracket asynchronous execution and then assert the resulting state.
Test recursion behavior explicitly. Update a record in a way that causes related automation to fire and verify that the intended side effect occurs once. A passing single-record unit test does not prove that a trigger framework behaves correctly under re-entry.
Use bypass mechanisms as governed operational controls
Large implementations sometimes need a controlled way to bypass selected automation for data repair, migration, or integration scenarios. A bypass should be explicit, permissioned, auditable, and narrow. Custom metadata, custom permissions, or a framework-specific control can be safer than editing code or sprinkling hard-coded usernames through handlers.
Bypass logic should not become a silent escape hatch for broken automation. Define who can use it, under what procedure, what compensating validation is required, and how the data will be reconciled after the operation. If routine loads require bypassing the trigger to succeed, the trigger probably needs redesign.
Review automation ownership before adding another trigger
Before creating Apex, inventory the automation already attached to the object. A validation rule, record-triggered Flow, managed-package trigger, rollup, or integration update may already own part of the behavior. Adding a second implementation of the same business rule creates a split-brain design in which two mechanisms can disagree about timing, errors, and field ownership.
Choose an owner for each rule and document why it belongs in Apex rather than declarative automation. Apex is justified when the logic needs programmatic control, complex collection processing, reusable services, or behavior that is difficult to express safely in Flow. The decision should make the transaction easier to understand, not simply move complexity into code.
Prefer predictable ownership over clever frameworks
A sophisticated trigger framework can help large teams, but architecture quality comes from predictable behavior: one entry point, clear context-specific handlers, bulk operations, change detection, idempotent side effects, intentional async boundaries, and tests that represent real transaction patterns.
Across Salesforce implementations, the most dangerous trigger is rarely the longest one. It is the trigger whose side effects are difficult to explain. A strong pattern lets a developer answer what runs, why it runs, how many records it handles, what happens if it runs twice, and which work continues after the transaction commits.