Intermediate 25 min read

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...
    }
}
  1. // Growing increasingly hard to manage — the label warns what's coming.
  2. trigger OpportunityTrigger on Opportunity (before insert, before update, after insert) { — one trigger handling three events at once.
  3. if (Trigger.isBefore) { — branches on timing.
  4. for (Opportunity opp : Trigger.new) { — loops over the records.
  5. if (opp.Amount != null && opp.Amount > 100000) { — the original business rule.
  6. opp.Priority__c = 'High'; — applies it.
  7. } — closes the if.
  8. // ...five more unrelated rules added over time... — as more rules get added directly inside the trigger body, it becomes harder to read.
  9. } — closes the loop.
  10. } — closes the outer if.
  11. if (Trigger.isAfter && Trigger.isInsert) { — a second branch, for a different event.
  12. // ...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.
  13. } — closes the if.
  14. } — 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);
}
  1. public class OpportunityTriggerHandler { — a regular Apex class, not a trigger.
  2. public void beforeInsert(List<Opportunity> newOpportunities) { — a plain method, taking a plain List<Opportunity>.
  3. for (Opportunity opp : newOpportunities) { — the same loop as before.
  4. if (opp.Amount != null && opp.Amount > 100000) { — the same business rule.
  5. opp.Priority__c = 'High'; — the same effect.
  6. } — closes the if.
  7. } — closes the loop.
  8. } — closes the method.
  9. } — closes the class.
  10. (blank line)
  11. trigger OpportunityTrigger on Opportunity (before insert) { — the trigger itself is now nearly empty.
  12. new OpportunityTriggerHandler().beforeInsert(Trigger.new); — it just hands Trigger.new off to the handler.
  13. } — 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);

Salesforce: ○ Not connected Connect
APEX

Execution Results

Run your code to see the result.

Avoiding Logic in Triggers Quiz

1. Why is logic moved into a handler class instead of staying directly in the trigger body?

Log in to submit the quiz and save your score.

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.