Apex

Apex Trigger Frameworks Explained — Patterns, Best Practices and Implementation

Published 7 June 2026 · Updated 12 June 2026 · 10 min read · Intermediate

A trigger framework is the difference between Apex automation that is maintainable, testable and extensible versus a collection of ad-hoc trigger files that become increasingly dangerous to modify as an org grows.

Why frameworks matter

Without a framework, triggers accumulate organically. Developer A writes one trigger for field defaulting. Developer B writes another for notifications. Developer C writes a third for integration callouts. All three are on the same object. Salesforce executes them in an unpredictable order. One trigger’s DML causes another to fire. A bug in one trigger affects all three. Testing any of them requires testing all of them.

A trigger framework imposes structure that prevents this entropy.

The minimal framework pattern

The minimal implementation has three components: one trigger per object (the dispatcher), a handler class (the logic), and a recursion guard.


// AccountTrigger.trigger — the dispatcher (nothing else here)
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert,  after update,  after delete, after undelete
) {
AccountTriggerHandler handler = new AccountTriggerHandler();
handler.dispatch();
}

```apex

// AccountTriggerHandler.cls — all logic lives here
public with sharing class AccountTriggerHandler {
    // Recursion guard
private static Boolean isRunning = false;
public void dispatch() {
    if (isRunning) return;
    isRunning = true;
    try {
        if (Trigger.isBefore) {
            if (Trigger.isInsert) onBeforeInsert(Trigger.new);
            if (Trigger.isUpdate) onBeforeUpdate(Trigger.new, Trigger.oldMap);
        }
        if (Trigger.isAfter) {
            if (Trigger.isInsert) onAfterInsert(Trigger.new, Trigger.newMap);
            if (Trigger.isUpdate) onAfterUpdate(Trigger.new, Trigger.newMap, Trigger.oldMap);
        }
    } finally {
        isRunning = false;
    }
}

private void onBeforeInsert(List<Account> newAccounts) {
    // Default field values, set computed fields
    AccountService.defaultFields(newAccounts);
}

private void onAfterInsert(List<Account> newAccounts, Map<Id, Account> newMap) {
    // Create related records, send notifications
    AccountService.createDefaultContacts(newAccounts);
}

private void onBeforeUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
    AccountService.handleStatusChange(newAccounts, oldMap);
}

private void onAfterUpdate(
    List<Account> newAccounts,
    Map<Id, Account> newMap,
    Map<Id, Account> oldMap
) {
    AccountService.syncRelatedOpportunities(newAccounts, oldMap);
}

## The Service class pattern

The handler dispatches to service classes that each own a specific domain
of business logic. This keeps the handler as a routing layer and the
service classes as the testable units of logic.

```apex

public with sharing class AccountService {
public static void defaultFields(List<Account> accounts) {
for (Account acc : accounts) {
if (acc.Rating == null) acc.Rating = 'Warm';
}
}

public static void handleStatusChange(
    List<Account> accounts,
    Map<Id, Account> oldMap
) {
    List<Account> changed = new List<Account>();
    for (Account acc : accounts) {
        if (acc.AccountSource != oldMap.get(acc.Id).AccountSource) {
            changed.add(acc);
        }
    }
    if (!changed.isEmpty()) {
        // Handle the subset of changed records
    }
}

}

## The bypass mechanism

Every framework should include a way to disable trigger execution for
specific contexts — data migrations, bulk loads, or emergency maintenance.

```apex

public with sharing class TriggerBypass {
public static Boolean isBypassed(String handlerName) {
// Query Custom Metadata: Trigger_Bypass__mdt
// Return true if bypass is active for this handler
return Trigger_Bypass__mdt.getInstance(handlerName)?.Active__c == true;
}
}

This allows an admin to activate a Custom Metadata bypass record without
deploying code — zero-downtime trigger disabling.

Test your knowledge — Apex

10 questions · Basic to Advanced

0 / 10 correct

Frequently asked questions

What is an Apex trigger framework?

An Apex trigger framework is an architectural pattern that enforces a single trigger per object, routes execution to a handler class containing all business logic, provides recursion prevention, and makes trigger logic unit-testable independently of the trigger context.

What is trigger recursion and how do you prevent it?

Trigger recursion occurs when DML inside a trigger causes the same object's trigger to fire again, potentially creating an infinite loop or double-executing logic. The standard prevention mechanism is a static boolean flag that is checked at the start of handler execution and set to true after the first run.

Why should a trigger be a thin dispatcher rather than contain business logic?

Keeping all logic in a handler class means handler methods can be called directly in unit tests without needing to instantiate a trigger context, which makes tests simpler and more reliable. Logic written directly in the trigger body cannot be tested this way.

What is the difference between Trigger.new and Trigger.old?

Trigger.new contains the current (new or updated) state of records being processed. Trigger.old contains the state before the DML operation and is available in update and delete contexts. Comparing Trigger.newMap and Trigger.oldMap is the standard way to detect field changes.

What is a trigger bypass mechanism and when should I use one?

A bypass mechanism — typically a Custom Metadata record — allows specific users, profiles, or integration contexts to execute DML without firing trigger automation. It is most useful during initial data migrations, bulk loads, or emergency maintenance where trigger side effects are not wanted.

How should bulkification be implemented in a trigger handler?

Collect all relevant IDs before any loop, execute a single SOQL query to retrieve all related records, store results in a Map keyed by ID, and reference the Map inside the loop. This pattern handles 1 record or 200 records with the same number of SOQL queries.