State managment in Salesforce

Trigger Hell and reentrant operations
We have all expereienced the problem of a trigger firing an update on an object, subsequently firing updates on the first object.... As our systems become more complex and our business requirements expand with automation - this problem grows from annoying to inconvenient to an absolute nightmare.
If you can't run a bulk update operation in your instance with a size limit greater than 100 - you have a reentrant problem!
Often the reaction (and recommendation) is to introduce static class variables. Let's talk about why static class variables in Salesforce triggers might seem like an easy fix but really introduce some nasty pitfalls—and then look at an alternate, last-resort approach using a Map and the Request class.
Why Static Variables Aren't Always the Best Choice
A lot of developers lean on static class variables to manage state across trigger executions. For example, you might see code like this:
public class TriggerHandler {
public static Boolean isTriggerDisabled = false;
public static void handleRecords(List records) {
if (isTriggerDisabled) {
return;
}
// Trigger logic here...
}
}
At first glance, it looks neat. But remember: static variables in Apex persist across the entire transaction. That means if one trigger sets isTriggerDisabled to true, it affects every subsequent trigger running in the same context—even if they should be processing. This transaction-wide behavior can lead to unexpected bugs, especially when multiple triggers or operations run within a single transaction.
Instead of relying on static booleans, you can try a different method. By using a Set to store processed identifiers—and tapping into the Request class—you can isolate logic per request. This ensures that trigger logic only executes once per unique request, rather than being affected by a global static flag.
Here's an example using the Request class:
public class TriggerHandler {
// Store a composite key for each processed request
private static Set processedRequests = new Set();
public static void handleRecords(List records) {
// Retrieve the current Request object
Request req = Request.getRequest();
// Extract the Quiddity from the Request object
String quiddity = req.getQuiddity(); // 'Quiddity' is an additional property to ensure uniqueness
// Combine them into a composite key
String key = quiddity;
// If this request (identified by the composite key) has already been processed, skip execution
if (processedRequests.contains(key)) {
return;
}
// Perform trigger logic here...
// Mark this request as processed
processedRequests.add(key);
}
}
Quiddity is an additional qualifier used to ensure uniqueness of each request. By combining Quiddity with the Request's unique properties, you can distinguish between different requests even in edge cases where they share some of the same identifiers.
A Word of Caution
While this Set-based approach works to prevent duplicate trigger execution, it's not without its pitfalls:
-
Global State Management:
You're still using a static Set, which introduces global state. If not handled carefully, it can lead to unforeseen side effects across different parts of your application. -
Potential Memory Issues:
Keeping track of many composite keys in memory might lead to higher memory consumption, which could hit governor limits in very busy transactions. -
Debugging Complexity:
Tracking issues in global state logic can be tricky, making this technique a last resort.
Optimising with the StateManager Class
If you do find yourself needing this type of state management, it’s important to make it as efficient and robust as possible. Let’s look at a an approach using a StateManager class. This will manage your state and ensure that logic is executed only once per request, and it will do so elegantly and efficiently.
public static class StateManager {
private static Map stateMap = new Map();
private StateManager() {}
public static Boolean hasRunQuddity(SObjectType sobjectType) {
Request req = Request.getRequest();
Quiddity currentQuiddity = req.getQuiddity();
// Retrieve or initialize the inner map
Quiddity alreadyRun = stateMap.get(sobjectType);
// Check if logic already ran
Boolean isAlreadyProcessed = alreadyRun == currentQuiddity;
// Only update the stateMap if needed
if (!isAlreadyProcessed) {
stateMap.put(sobjectType, currentQuiddity);
}
return isAlreadyProcessed;
}
public static Boolean hasRunAny(SObjectType sobjectType) {
Request req = Request.getRequest();
Quiddity currentQuiddity = req.getQuiddity();
// Check if logic has already run for the given SObjectType
Boolean isAlreadyProcessed = stateMap.containsKey(sobjectType);
// Only update the stateMap if it's not processed
if (!isAlreadyProcessed) {
stateMap.put(sobjectType, currentQuiddity);
}
return isAlreadyProcessed;
}
}
With the StateManager class in place, your trigger logic can become:
public class AccountTriggerHandler {
public static void handleAccounts(List accounts) {
// Check if the Account trigger logic has already run for this request with the Quiddity.TRIGGER qualifier.
if (!StateManager.hasRunQuddity(Account.SObjectType)) {
// Execute your Account trigger logic...
}
}
}
If you prefer to run once based on any Quiddity:
public class AccountTriggerHandler {
public static void handleAccounts(List accounts) {
// Check if the Account trigger logic has already run for this request with the Quiddity.TRIGGER qualifier.
if (!StateManager.hasRunAny(Account.SObjectType)) {
// Execute your Account trigger logic...
}
}
}
Summary
State Management is never a fun task—re-architecting your execution flow is always preferable. In cases where that’s not feasible, an appropriately implemented StateManager is your best bet for keeping reentrant trigger hell at bay.
Final Thoughts
While static state can offer temporary relief in Salesforce triggers, it's not without its pitfalls. If you're
stuck with reentrant triggers, the StateManager class provides an efficient and maintainable way to ensure your logic
only runs once per unique request, thus avoiding issues and improving performance
The Request class also has the RequestId(), next post
I'll use this to provide some transaction mapping and handling that I first used back in my SQL Server days - its going to be fun so stay tuned!