Writing an Apex trigger is mechanically simple. Writing one that survives production data volumes, bulk operations, and three years of evolution is harder. Follow the conventions: one trigger per object, thin dispatcher with a handler class, bulkified logic, recursion control, and comprehensive tests. Skip any of these and the trigger becomes a debugging hotspot.
- Confirm Flow cannot do the job first
Record-triggered Flow handles most save-event logic now. Use Apex triggers only when the logic exceeds Flow's expressiveness or performance envelope. This conversation with the architect saves weeks of rework later.
- Create the trigger file with all events declared
One trigger per object, declaring all relevant events at once: trigger AccountTrigger on Account (before insert, after insert, before update, after update, before delete, after delete, after undelete). This is the convention and the entry point.
- Build the handler class with one method per context
Separate file: AccountTriggerHandler.cls. Public methods for each event-context pair (handleBeforeInsert, handleAfterUpdate). The trigger delegates to these methods based on Trigger.isBefore, Trigger.isInsert, etc.
- Bulkify the logic with collections and maps
Inside each handler method, collect record IDs, run SOQL outside loops, build maps of related data, process each Trigger.new record using the maps, then do DML outside the loop. This is the single most important discipline.
- Add recursion control via static boolean or framework
Add a static boolean to the handler class (alreadyRan = false). Check it at entry to skip re-execution. For more complex needs, adopt a TriggerHandler framework like Hari Krishnan or Kevin Poorman patterns.
- Write the test class with bulk and edge cases
Create AccountTriggerHandlerTest.cls. Use @testSetup for common data. Test the bulk case (insert 200 records). Test single-record cases. Test boundary conditions. Use System.assertEquals on the expected post-save state.
- Deploy with the test class and confirm coverage
Deploy trigger and test class together. Confirm coverage above 75 percent. Run the tests in production after deployment to verify behavior matches sandbox. Set up a CI/CD pipeline that runs tests on every change.
- Monitor the trigger via Apex Jobs and exception logs
Setup > Apex Jobs shows recent execution. Setup > Email Logs shows uncaught exceptions. Build a Custom Error_Log object and log caught exceptions to it for better debuggability than the default exception email.
before insert, after insert, before update, after update, before delete, after delete, after undelete. Declare all events in one trigger per object.
Separate Apex class that holds the actual logic. The trigger should be a thin dispatcher that delegates to the handler.
Static boolean in the handler class or a TriggerHandler framework. Without it, save-cascade scenarios re-fire triggers infinitely.
- Multiple triggers on the same object execute in non-deterministic order. Adopt the one-trigger-per-object convention from day one to avoid hard-to-debug ordering issues later.
- Triggers fire on batches of up to 200 records. SOQL or DML inside a loop hits governor limits on the first bulk operation. Bulkification is mandatory, not optional.
- Triggers can fire themselves recursively when their logic updates records they match. Use static booleans or a TriggerHandler framework to prevent infinite loops.
- Before triggers can modify Trigger.new directly; the changes save automatically. After triggers cannot modify Trigger.new because the records are already saved.
- Test coverage minimum is 75 percent across all Apex, including triggers. Write tests with meaningful assertions, not empty methods that just exercise lines for coverage credit.