Intermediate 30 min read

Build the Trigger Handler

By the end of this lesson, you'll be able to:

  • Write a thin trigger routing to a handler class, following Module 24's pattern
  • Implement the handler's afterUpdate method for the basic case
  • Explain why Trigger.oldMap is needed for this specific requirement

Prerequisites: "Design the Requirement"

The thin trigger

trigger OpportunityTrigger on Opportunity (after update) {
    new OpportunityTriggerHandler().afterUpdate(Trigger.new, Trigger.oldMap);
}
  1. trigger OpportunityTrigger on Opportunity (after update) { — exactly Module 24's "Avoiding Logic in Triggers" pattern.
  2. new OpportunityTriggerHandler().afterUpdate(Trigger.new, Trigger.oldMap); — the trigger itself does nothing but hand off to the handler, passing both Trigger.new and Trigger.oldMap since this requirement needs to compare old and new values.
  3. } — closes the trigger.

Detecting the actual change

public class OpportunityTriggerHandler {
    public void afterUpdate(List<Opportunity> newOpportunities, Map<Id, Opportunity> oldMap) {
        for (Opportunity opp : newOpportunities) {
            Opportunity oldOpp = oldMap.get(opp.Id);
            Boolean justWon = opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won';
            Boolean justUnwon = opp.StageName != 'Closed Won' && oldOpp.StageName == 'Closed Won';

            if (justWon) {
                System.debug('Opportunity ' + opp.Id + ' just became Closed Won.');
            }
            if (justUnwon) {
                System.debug('Opportunity ' + opp.Id + ' is no longer Closed Won.');
            }
        }
    }
}
  1. public class OpportunityTriggerHandler { — the handler class.
  2. public void afterUpdate(List<Opportunity> newOpportunities, Map<Id, Opportunity> oldMap) { — the method the trigger calls.
  3. for (Opportunity opp : newOpportunities) { — loops over the updated records.
  4. Opportunity oldOpp = oldMap.get(opp.Id); — this is Module 24's Trigger.oldMap lesson doing real work — looks up this specific record's prior version.
  5. Boolean justWon = opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won'; — justWon distinguishes "actually changed into Closed Won" from "was already Closed Won and something unrelated changed."
  6. Boolean justUnwon = opp.StageName != 'Closed Won' && oldOpp.StageName == 'Closed Won'; — justUnwon catches the reverse transition.
  7. (blank line)
  8. if (justWon) { — checks the first case.
  9. System.debug('Opportunity ' + opp.Id + ' just became Closed Won.'); — logs it.
  10. } — closes the if.
  11. if (justUnwon) { — checks the second case.
  12. System.debug('Opportunity ' + opp.Id + ' is no longer Closed Won.'); — logs it.
  13. } — closes the if.
  14. } — closes the loop.
  15. } — closes the method.
  16. } — closes the class.

Why this comparison matters

Without comparing against oldMap, every update on an Opportunity already sitting at Closed Won (say, just updating its description) would look identical to one that just became Closed Won — double-counting the same Amount into the rollup every time the record is touched. Catching only the actual transition is what keeps the rollup correct over time.

Exercise

Given opp and oldOpp, write a Boolean expression amountChanged that is true only when the Amount field's value actually changed.

Show hint

opp.Amount != oldOpp.Amount

Salesforce: ○ Not connected Connect
APEX

Execution Results

Run your code to see the result.

Build the Trigger Handler Quiz

1. Why compare opp.StageName against oldMap.get(opp.Id).StageName instead of just checking opp.StageName == 'Closed Won'?

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

This lesson writes the trigger and handler skeleton, detecting exactly when an Opportunity newly becomes (or stops being) Closed Won.