- 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
Queueableas your default asynchronous tool. Avoid writing new@futuremethods.
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
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
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.
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.
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.newholds 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.
- 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.
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
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
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.oldMapandTrigger.newMapto detect real value changes.
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.
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.