Non-bulkified code, several triggers on one object, hardcoded IDs, weak error handling: these are the trigger mistakes I keep running into, and each one costs you performance, a clean deployment, or maintenance time.
Why triggers matter
Apex triggers run your custom logic during record save events: insert, update, delete. That power is the problem. A badly written trigger gives you governor limit failures, data changes nobody expected, and code the next developer cannot safely touch.
Top mistakes and how to fix them
1. Choosing Apex over Flow when a declarative solution fits
Reach for a Record-Triggered Flow on simple record automation. An admin can maintain it, and it is less debt. Apex earns its place when declarative tools cannot meet the requirement: complex processing, heavy logic, integrations.
2. No trigger framework (mixing logic in the trigger)
DML, SOQL, validations and business logic crammed into the trigger body are hard to test and hard to reuse. Keep one trigger per object and push the work into handler or service classes.
3. Not bulkifying the trigger
Never assume the trigger got one record. Handle the collection, and keep SOQL and DML out of the loop.
// Bad: SOQL inside loop for (Account a : Trigger.new) { Account acc = [SELECT Id, Name FROM Account WHERE Id = :a.Id]; // do work }
// Good: gather ids, then query once Set accIds = new Set(); for (Account a : Trigger.new) accIds.add(a.Id); Map accounts = new Map([SELECT Id, Name FROM Account WHERE Id IN :accIds]);
4. Not checking trigger context
Guard the logic with Trigger.isBefore / Trigger.isAfter and Trigger.isInsert / Trigger.isUpdate so it runs only in the context you meant it to.
trigger OpportunityTrigger on Opportunity (before insert, before update) { if (Trigger.isBefore && Trigger.isInsert) { // Insert-specific logic } else if (Trigger.isBefore && Trigger.isUpdate) { // Update-specific logic } }
5. Recursive trigger execution
Recursive updates can cause infinite loops. A handler framework, or static variables in a utility class, blocks the re-entry and keeps control of execution.
6. Hardcoding IDs or organization-specific values
Record type IDs, user IDs and other environment-specific values do not belong in code. Retrieve RecordType by DeveloperName, or keep the configuration in Custom Metadata or Custom Settings.
7. Poor exception handling
Wrap DML in try-catch and put a meaningful addError message on the record. The user gets something to act on, and the data is not left half processed.
try { update accounts; } catch (DmlException e) { for (Account a : Trigger.new) { a.addError('Failed to update account: ' + e.getMessage()); } }
8. Multiple triggers on the same object
Two triggers on one object fire in an order you do not control. One trigger per object, with a handler class orchestrating the logic, gets that order back.
9. Missing or weak test coverage
Cover every branch of the trigger logic and the edge cases with test classes. Assert on real outcomes, and stop the tests depending on hardcoded data.
10. Not following naming conventions
Name the trigger for the object it runs on (e.g., AccountTrigger), and name the handler to match. Readable names are what keep the code maintainable.
Best practices checklist
- Use a Flow if a Flow can do it.
- One trigger per object, logic in handler classes.
- Bulkify queries and DML. Never in a loop.
- Hold recursion back with static flags.
- Look values up instead of hardcoding IDs.
- Catch exceptions and show a readable error.
- Test every branch.
- Follow the org's naming and code style.
Conclusion: why this matters
None of this is clever work. It is the difference between a trigger that survives a data load and one that dies on a governor limit, between an org that is easy to support and one full of debt. Moving the right logic to Flow gives admins and architects more agility. Handler patterns and bulk-safe code give developers reliable deployments.
If you want help refactoring triggers or bulkifying code, a code review is a good place to start.
Leave a Comment