Avoiding Logic in Triggers
By the end of this lesson, you'll be able to:
- Explain why business logic shouldn't live directly inside a trigger body
- Move a trigger's logic into a handler class
- Recognize the trigger itself as a thin entry point, not the place for real work
Prerequisites: "Your First Trigger"
The problem with logic-heavy triggers
// Growing increasingly hard to manage
trigger OpportunityTrigger on Opportunity (before insert, before update, after insert) {
if (Trigger.isBefore) {
for (Opportunity opp : Trigger.new) {
if (opp.Amount != null && opp.Amount > 100000) {
opp.Priority__c = 'High';
}
// ...five more unrelated rules added over time...
}
}
if (Trigger.isAfter && Trigger.isInsert) {
// ...yet more logic...
}
}
// Growing increasingly hard to manage— the label warns what's coming.trigger OpportunityTrigger on Opportunity (before insert, before update, after insert) {— one trigger handling three events at once.if (Trigger.isBefore) {— branches on timing.for (Opportunity opp : Trigger.new) {— loops over the records.if (opp.Amount != null && opp.Amount > 100000) {— the original business rule.opp.Priority__c = 'High';— applies it.}— closes theif.// ...five more unrelated rules added over time...— as more rules get added directly inside the trigger body, it becomes harder to read.}— closes the loop.}— closes the outerif.if (Trigger.isAfter && Trigger.isInsert) {— a second branch, for a different event.// ...yet more logic...— critically, impossible to unit test in isolation, since a trigger can't be called directly the way a regular method (Module 9) can.}— closes theif.}— closes the trigger.
Moving logic into a handler class
public class OpportunityTriggerHandler {
public void beforeInsert(List<Opportunity> newOpportunities) {
for (Opportunity opp : newOpportunities) {
if (opp.Amount != null && opp.Amount > 100000) {
opp.Priority__c = 'High';
}
}
}
}
trigger OpportunityTrigger on Opportunity (before insert) {
new OpportunityTriggerHandler().beforeInsert(Trigger.new);
}
public class OpportunityTriggerHandler {— a regular Apex class, not a trigger.public void beforeInsert(List<Opportunity> newOpportunities) {— a plain method, taking a plainList<Opportunity>.for (Opportunity opp : newOpportunities) {— the same loop as before.if (opp.Amount != null && opp.Amount > 100000) {— the same business rule.opp.Priority__c = 'High';— the same effect.}— closes theif.}— closes the loop.}— closes the method.}— closes the class.- (blank line)
trigger OpportunityTrigger on Opportunity (before insert) {— the trigger itself is now nearly empty.new OpportunityTriggerHandler().beforeInsert(Trigger.new);— it just handsTrigger.newoff to the handler.}— closes the trigger.
All the actual logic lives in a regular Apex class, which — critically — can be called directly and tested like any other class from Modules 9-17.
Why this separation matters
This is Module 14's Single Responsibility Principle, applied to triggers specifically: the trigger's one job is "notice the event and hand off," while the handler class's job is "actually decide what to do." A later Testing module will show exactly why this separation makes triggers realistically testable — testing OpportunityTriggerHandler.beforeInsert() directly, with a plain List<Opportunity>, is far simpler than trying to test trigger behavior indirectly through actual DML.
Exercise
Refactor this trigger to move its logic into a new CaseTriggerHandler class with a beforeInsert method, keeping the trigger itself thin.
Show hint
The trigger should just call new CaseTriggerHandler().beforeInsert(Trigger.new);
Avoiding Logic in Triggers Quiz
My Notes
Log in to keep private notes on this lesson.
Questions about this lesson
No questions yet — be the first to ask.
Log in to ask a question about this lesson.
Summary
A trigger should stay thin — a simple entry point that hands off to a regular Apex class — because logic buried directly inside a trigger is hard to test, hard to reuse, and hard to read at scale.