Intermediate 30 min read

Bulkify the Logic

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

  • Collect the Account Id and Amount delta for every affected Opportunity
  • Query and update the affected Accounts in a single bulk operation
  • Apply Module 25's "collect, then act once" pattern to this specific feature

Prerequisites: "Build the Trigger Handler"

Collecting the amount to adjust, per Account

public void afterUpdate(List<Opportunity> newOpportunities, Map<Id, Opportunity> oldMap) {
    Map<Id, Decimal> amountAdjustmentByAccountId = new Map<Id, Decimal>();

    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';
        Decimal amount = opp.Amount == null ? 0 : opp.Amount;

        if (justWon) {
            addAdjustment(amountAdjustmentByAccountId, opp.AccountId, amount);
        } else if (justUnwon) {
            addAdjustment(amountAdjustmentByAccountId, opp.AccountId, -amount);
        }
    }
    // ... apply the adjustments (next section)
}

private void addAdjustment(Map<Id, Decimal> adjustments, Id accountId, Decimal amount) {
    if (! adjustments.containsKey(accountId)) {
        adjustments.put(accountId, 0);
    }
    adjustments.put(accountId, adjustments.get(accountId) + amount);
}
  1. public void afterUpdate(List<Opportunity> newOpportunities, Map<Id, Opportunity> oldMap) { — the method.
  2. Map<Id, Decimal> amountAdjustmentByAccountId = new Map<Id, Decimal>(); — this is Module 16's "Maps of sObjects" pattern — one entry per affected Account.
  3. (blank line)
  4. for (Opportunity opp : newOpportunities) { — loops over the updated records — pure in-memory work so far.
  5. Opportunity oldOpp = oldMap.get(opp.Id); — the prior version.
  6. Boolean justWon = opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won'; — the same transition check as before.
  7. Boolean justUnwon = opp.StageName != 'Closed Won' && oldOpp.StageName == 'Closed Won'; — the reverse.
  8. Decimal amount = opp.Amount == null ? 0 : opp.Amount; — Module 6's ternary operator, defaulting a null Amount to 0.
  9. (blank line)
  10. if (justWon) { — the won case.
  11. addAdjustment(amountAdjustmentByAccountId, opp.AccountId, amount); — adds the amount to this Account's running total.
  12. } else if (justUnwon) { — the unwon case.
  13. addAdjustment(amountAdjustmentByAccountId, opp.AccountId, -amount); — subtracts it back out.
  14. } — closes the if/else if.
  15. } — closes the loop.
  16. // ... apply the adjustments (next section) — the next step, covered separately.
  17. } — closes the method.
  18. (blank line)
  19. private void addAdjustment(Map<Id, Decimal> adjustments, Id accountId, Decimal amount) { — a small helper.
  20. if (! adjustments.containsKey(accountId)) { — the first time this Account is seen...
  21. adjustments.put(accountId, 0); — ...its running total starts at 0.
  22. } — closes the if.
  23. adjustments.put(accountId, adjustments.get(accountId) + amount); — adds this adjustment, so several Opportunities for the same Account in one transaction combine into a single running total.
  24. } — closes the helper.

Applying the adjustments in one bulk update

private void applyAdjustments(Map<Id, Decimal> amountAdjustmentByAccountId) {
    if (amountAdjustmentByAccountId.isEmpty()) {
        return;
    }

    List<Account> accounts = [
        SELECT Id, Total_Won_Revenue__c
        FROM Account
        WHERE Id IN :amountAdjustmentByAccountId.keySet()
    ];

    for (Account acc : accounts) {
        Decimal current = acc.Total_Won_Revenue__c == null ? 0 : acc.Total_Won_Revenue__c;
        acc.Total_Won_Revenue__c = current + amountAdjustmentByAccountId.get(acc.Id);
    }

    update accounts;
}
  1. private void applyAdjustments(Map<Id, Decimal> amountAdjustmentByAccountId) { — the method.
  2. if (amountAdjustmentByAccountId.isEmpty()) { — if nothing needs adjusting...
  3. return; — ...exits early, avoiding a wasted query and update.
  4. } — closes the if.
  5. (blank line)
  6. List<Account> accounts = [ — this is Module 25's core pattern applied directly — one query.
  7. SELECT Id, Total_Won_Revenue__c — the fields needed.
  8. FROM Account — the object.
  9. WHERE Id IN :amountAdjustmentByAccountId.keySet() — Module 16's keySet(), fetching exactly the affected Accounts.
  10. ]; — closes the query.
  11. (blank line)
  12. for (Account acc : accounts) { — loops over the affected accounts, in memory.
  13. Decimal current = acc.Total_Won_Revenue__c == null ? 0 : acc.Total_Won_Revenue__c; — defaults a null rollup to 0.
  14. acc.Total_Won_Revenue__c = current + amountAdjustmentByAccountId.get(acc.Id); — applies this Account's adjustment.
  15. } — closes the loop.
  16. (blank line)
  17. update accounts; — one update, regardless of whether 1 or 200 Opportunities changed in this transaction — exactly what makes this feature bulk-safe.
  18. } — closes the method.

Exercise

As a comment, explain why amountAdjustmentByAccountId.isEmpty() is checked before running the query and update.

Show hint

Think about a batch of Opportunity updates where none of them actually changed Closed Won status.

APEX

Bulkify the Logic Quiz

1. How many DML statements does applyAdjustments run, regardless of how many Opportunities changed?

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 turns the previous lesson's per-record debug statements into a real, bulk-safe rollup — querying and updating Accounts once, regardless of how many Opportunities changed.