Skip to main content

Salesforce Trigger Frameworks: Bulkification & Reentrancy Explained

⚡ 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.