Skip to main content

Salesforce Trigger Frameworks: Bulkification & Reentrancy Explained

Updated Add a comment
⚡ 1-Minute Summary:
  • Governor limits act as a transaction budget shared across your code, flows, and packages.
  • Strictly enforce one trigger per object. Keep logic out of the trigger itself and build a bypass switch on day one.
  • Use Queueable as your default asynchronous tool. Avoid writing new @future methods.

Module Map: Programmatic Logic & Asynchronous Execution

  • Trigger frameworks (bulkification, reentrancy)
  • Synchronous vs. Asynchronous execution
  • @future / Batch / Queueable / Scheduled Apex
  • Governor limits (the platform's fairness budget)
  • Exception handling & logging
  • Platform Cache

Quick Concept: Queues vs. Public Groups

Q: What is the core difference between queues and public groups?

A: Think of it as ownership versus audience.

  • Queues own records. Unassigned work (like cases, leads, or custom objects) sits in a queue until a member claims it.
  • Public Groups own nothing. They are purely an audience list used to grant visibility via sharing rules, list views, and folder access.

In four words: Ownership Queue, Audience Group.

Trigger Handler Frameworks, Bulkification, and Reentrancy

💬 In plain words: You should only ever have one trigger per object, and that trigger should be completely "dumb." It merely routes data to a handler class where the actual logic lives. Everything must be written to handle lists of records (bulkification). Finally, a reentrancy guard stops the trigger from endlessly re-running itself if its own updates cause it to fire again.
📌 The Problem: Imagine having three different triggers on the Account object that fire in a random order. One of them might read stale data because another trigger hasn't finished updating yet.

The Fix: Consolidate to ONE AccountTrigger that calls an AccountTriggerHandler. Inside the handler, methods run in a guaranteed, fixed sequence. A static guard ensures that if a workflow rule re-fires the update, the entire process doesn't execute twice.
🎬 Real-Life Example: The Org With Four Delivery Triggers
Over three years, four different developers each added their own trigger on Delivery__c.
  • The Bad Way: Four separate triggers. Each contains logic, and each queries data inside its own loop. The firing sequence between them is completely random.
  • The Pain: Firing order changes between saves. Bugs randomly appear and disappear. The same exact query burns your governor limits four times. Nobody dares to touch the code.
  • The Good Way: Consolidate to one logic-less trigger. It calls DeliveryTriggerHandler.run(). The handler routes the code by context (e.g., beforeInsert, afterUpdate). Data is queried once into Maps, and a static guard prevents endless loops.
  • The Payoff: One entry point, guaranteed execution order, and a single query pass. When the next developer needs to add an update, they simply add a method to the handler—not a fifth trigger.
🧠 Core Rule: Dumb trigger, smart handler. One trigger per object. Lists everywhere.

The Core Principles:

  • Zero logic in the trigger body.
  • All logic goes in a handler class, routed by context (before/after, insert/update/delete/undelete).
  • Bulkification means every code path assumes Trigger.new holds up to 200 records. Query once into Maps, do one DML statement per object at the end.
  • Reentrancy (Recursion) is controlled with static guards. Do not use a naive boolean like hasRun = true, because this will break legitimate secondary passes (like batch retries or workflow field updates). Track processed Record Ids instead.
Trigger Handler Frameworks, Bulkification, Reentrancy
🧭 360 Card — Trigger Handler Frameworks
  • Rule: One trigger per object, zero logic inside it, and implement a bypass switch on day one.
  • Gain: Predictable firing order, easily readable code, and reusable services that flow actions, batch jobs, and triggers can all share.
  • Price: A framework is custom code you must own and maintain. New developers must learn your framework before adding logic.
  • Limits: Having multiple triggers on one object results in an undefined execution order. Triggers always run in system mode. Static variables live for the duration of the entire transaction, not just a single trigger invocation.
  • At Volume: A 10,000-record data load processes in 50 separate 200-record chunks. Your framework must handle this without skipping chunks.
⚠ INTERVIEW TRAP: Suggesting a static boolean guard (e.g., public static Boolean hasRun = false;) to stop recursion is a classic wrong answer. During a bulk data load, this allows the first 200 records to process, but completely skips the remaining chunks within the same transaction.

Core Q&A

Q: Sketch a minimal trigger handler pattern and explain what each part defends against.
🎯 Say this first: "Use one logic-less trigger per object that dispatches to a handler class. The handler processes Trigger.new as a list, includes a bypass switch, and uses a static Set of Ids to guard against re-entry."

A: The trigger instantly delegates all work. The handler opens one method per context. A static Set<Id> prevents records from being processed twice in the same transaction, while a bypass check respects custom settings used to disable triggers during heavy data loads.

Using a Set<Id> instead of a Boolean allows the same transaction to process different records that arrive via cascading updates, while strictly blocking recursion on the exact same rows.

// 1. ONE trigger for the object. It holds no logic.
trigger AccountTrigger on Account (before insert, before update, after update) {
    new AccountHandler().run();
}

public class AccountHandler {
    // Track Ids to prevent infinite recursion
    private static Set<Id> processed = new Set<Id>();

    public void run() {
        // 2. The bypass switch for safe data loads
        if (Bypass__c.getInstance().Skip_Triggers__c) return;

        // 3. Route by context
        if (Trigger.isBefore && Trigger.isUpdate) beforeUpdate();
        if (Trigger.isAfter  && Trigger.isUpdate) afterUpdate();
    }

    private void afterUpdate() {
        List<Account> todo = new List<Account>();
        
        for (Account a : (List<Account>) Trigger.new) {
            if (!processed.contains(a.Id)) {
                processed.add(a.Id); 
                todo.add(a); 
            }
        }
        
        // Perform bulk queries and DML on the 'todo' list here
    }
}

Scenario-Based Follow-Ups

Q1: Why is a 'static Boolean hasRun = true' logic a bug factory?

A1: Static variables live for the entire transaction, and one transaction often requires multiple handler passes.

  • If you load 10,000 records, Salesforce chunks them into 200-record batches within the same transaction. A boolean lets chunk #1 run and silently ignores chunks 2 through 50.
  • Workflow field updates or cascading updates from related objects are legitimate reasons for a trigger to run a second time on new records.
  • Solution: Guard using processed record Ids, or by comparing Trigger.oldMap and Trigger.newMap to detect real value changes.
Q2: Two teams keep colliding in one handler with merge conflicts. How do you scale this?

A2: Transition from a single monolithic handler to a registry of ordered action classes.

  • The main handler simply iterates through a list of single-purpose classes. Each class implements an interface with a run(context) method and is owned by one specific team.
  • Register these classes using Custom Metadata Types to dictate their execution order and provide an active/inactive toggle.
  • Conflicts drop because teams only touch their respective classes. In an emergency, you can disable a specific action via Custom Metadata without needing a deployment.
Q3: Why is a trigger bypass switch a hard requirement?

A3: Because the worst possible time to invent one is at midnight during a 5-million-record data migration.

  • A bypass switch (like a Custom Permission or Hierarchy Custom Setting) allows massive data loads to run quickly and cleanly.
  • It acts as an emergency kill switch if a bug is actively corrupting data in production.
  • Build it into the very top of your handler on day one so you can turn automation on or off globally, per profile, or per user.

Was this article helpful?

Comments

Questions, corrections and your own experience are all welcome.