Intermediate 35 min read

Trigger Framework Fundamentals

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

  • Explain the "one trigger per object" convention and why it exists
  • Route different event types to different handler methods from a single trigger
  • Recognize what a full trigger framework adds beyond this module's simple handler pattern

Prerequisites: "Order of Execution"

The problem with multiple triggers on one object

Nothing stops a codebase from having AccountTrigger1, AccountTrigger2, and AccountTrigger3 all firing on the same Account object — but Salesforce doesn't guarantee the order multiple triggers on the same object run in relative to each other. This makes behavior genuinely unpredictable as more triggers accumulate over time.

One trigger, routed by event

trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
    AccountTriggerHandler handler = new AccountTriggerHandler();

    if (Trigger.isBefore && Trigger.isInsert) {
        handler.beforeInsert(Trigger.new);
    }
    if (Trigger.isBefore && Trigger.isUpdate) {
        handler.beforeUpdate(Trigger.new, Trigger.oldMap);
    }
    if (Trigger.isAfter && Trigger.isInsert) {
        handler.afterInsert(Trigger.new);
    }
    if (Trigger.isAfter && Trigger.isUpdate) {
        handler.afterUpdate(Trigger.new, Trigger.oldMap);
    }
}
  1. trigger AccountTrigger on Account (before insert, before update, after insert, after update) { — exactly one trigger per object, handling every event.
  2. AccountTriggerHandler handler = new AccountTriggerHandler(); — one handler instance.
  3. (blank line)
  4. if (Trigger.isBefore && Trigger.isInsert) { — routes to a dedicated method per event combination.
  5. handler.beforeInsert(Trigger.new); — the before-insert logic.
  6. } — closes the if.
  7. if (Trigger.isBefore && Trigger.isUpdate) { — a different combination.
  8. handler.beforeUpdate(Trigger.new, Trigger.oldMap); — the before-update logic.
  9. } — closes the if.
  10. if (Trigger.isAfter && Trigger.isInsert) { — another combination.
  11. handler.afterInsert(Trigger.new); — the after-insert logic.
  12. } — closes the if.
  13. if (Trigger.isAfter && Trigger.isUpdate) { — the last combination.
  14. handler.afterUpdate(Trigger.new, Trigger.oldMap); — the after-update logic.
  15. } — closes the if.
  16. } — closes the trigger — this eliminates the ordering ambiguity of multiple separate triggers, while keeping each event's logic in its own clearly-named method.

The corresponding handler class

public class AccountTriggerHandler {
    public void beforeInsert(List<Account> newAccounts) {
        for (Account acc : newAccounts) {
            if (String.isBlank(acc.Industry)) {
                acc.Industry = 'Unknown';
            }
        }
    }

    public void beforeUpdate(List<Account> updatedAccounts, Map<Id, Account> oldMap) {
        // update-specific logic here
    }

    public void afterInsert(List<Account> newAccounts) {
        // after-insert-specific logic here
    }

    public void afterUpdate(List<Account> updatedAccounts, Map<Id, Account> oldMap) {
        // after-update-specific logic here
    }
}
  1. public class AccountTriggerHandler { — every method here can be called and tested directly.
  2. public void beforeInsert(List<Account> newAccounts) { — the before-insert handler.
  3. for (Account acc : newAccounts) { — the same default-Industry logic as earlier lessons.
  4. if (String.isBlank(acc.Industry)) { — checks for blank.
  5. acc.Industry = 'Unknown'; — applies the default.
  6. } — closes the if.
  7. } — closes the loop.
  8. } — closes the method.
  9. (blank line)
  10. public void beforeUpdate(List<Account> updatedAccounts, Map<Id, Account> oldMap) { — a separate method for update-specific logic.
  11. // update-specific logic here — a placeholder for that logic.
  12. } — closes the method.
  13. (blank line)
  14. public void afterInsert(List<Account> newAccounts) { — a separate method for after-insert logic.
  15. // after-insert-specific logic here — a placeholder.
  16. } — closes the method.
  17. (blank line)
  18. public void afterUpdate(List<Account> updatedAccounts, Map<Id, Account> oldMap) { — a separate method for after-update logic.
  19. // after-update-specific logic here — a placeholder.
  20. } — closes the method.
  21. } — closes the class.

This closing lesson's shape combines "one trigger per object" with Lesson 5's "avoid logic in triggers" into a single, coherent pattern.

What a full trigger framework adds

Real production orgs typically go further than this module's simple routing — a full trigger framework adds a shared base class handling the routing automatically (so every trigger's body becomes nearly identical), built-in recursion guards (Lesson 6, generalized), and a consistent way to bypass triggers during data loads or tests. This module's pattern is the genuine foundation those frameworks build on — understanding it well is what makes a full framework make sense later, rather than feeling like unnecessary extra machinery.

Exercise

Write a Case trigger routing before insert and after insert to a CaseTriggerHandler with matching method names.

Show hint

if (Trigger.isBefore && Trigger.isInsert) { handler.beforeInsert(Trigger.new); }

Salesforce: ○ Not connected Connect
APEX

Execution Results

Run your code to see the result.

Trigger Framework Fundamentals Quiz

1. Why does "one trigger per object" matter?

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 closing lesson formalizes the handler pattern from earlier in the module into "one trigger per object," routing every event type to its own handler method — the foundation real trigger frameworks build on.