Salesforce Trigger Framework (Code Version): A Recursion-Safe, Bulk-Safe, Scalable Architecture (Solve SOQL 101 error and DML 151 Error)
- Deepak Arora
- 4 minutes ago
- 3 min read
In our previous post, we shared the story of how we encountered trigger recursion, SOQL limit breaches, and performance issues when multiple automation layers (Flows, PB, Workflows, Integrations) acted on the same records.
As promised, here is the complete code version of the recursion-safe, enterprise-ready Salesforce Trigger Framework we implemented to solve this problem.
This design keeps the trigger clean, avoids repeated execution on the same records, separates logic by context (insert/update/delete), and ensures governor-limit-safe execution in every scenario.
Core Principles of the Framework
Before jumping into the code, here are the design principles:
Single Trigger Per Object
No mixing logic across multiple triggers.
Switch-based Context Handling
Clear separation for before insert, before update, after update, etc.
Recursion Control
Prevent the same record from being processed twice inside the same
transaction.
Bulk-Safe Design
All logic runs on lists/maps—no per-record SOQL or DML.
Cleaner Maintenance
Each event has its own handler method.
The Trigger (Generic, Clean, Structured)
trigger ObjName_trigger on ObjName (before delete, after delete, before
insert, after insert, before update, after update) {
Switch on Trigger.operationType {
when BEFORE_INSERT {
if (trigger_cntrl.is_before_insert) {
trigger_cntrl.beforeInsert(Trigger.New,Trigger.Old,
Trigger.Newmap,Trigger.Oldmap);
}
}
when BEFORE_UPDATE {
list<Obj> list_of_new_obj = new list<Obj>();
map<id,Obj> new_map_of_obj = new map<id,Obj>();
for(Obj current_Obj : trigger.new) {
if(!trigger_cntrl.set_b_update.contains(current_Obj.id)){
list_of_new_obj.add(current_Obj);
new_map_of_obj.put(current_Obj.id,current_Obj);
trigger_cntrl.set_b_update.add(current_Obj.id);
}
}
if(!list_of_new_obj.isempty()){
trigger_cntrl.beforeupdate(list_of_new_obj,Trigger.Old,
new_map_of_obj,Trigger.Oldmap);
}
}
when AFTER_INSERT {
list<Obj> list_of_new_Obj = new list<Obj>();
map<id,Obj> new_map_of_Obj = new map<id,Obj>();
for(Obj current_Obj : trigger.new) {
if(!trigger_cntrl.set_a_insert.contains(current_Obj.id)){
list_of_new_Obj.add(current_Obj);
new_map_of_Obj.put(current_Obj.id,current_Obj);
trigger_cntrl.set_a_insert.add(current_Obj.id);
}
}
if(!list_of_new_Obj.isempty()) {
trigger_cntrl.afterInsert(list_of_new_Obj,Trigger.Old,
new_map_of_Obj,Trigger.Oldmap);
}
}
when AFTER_UPDATE {
list<Obj> list_of_new_Obj = new list<Obj>();
map<id,Obj> new_map_of_Obj = new map<id,Obj>();
for(Objcurrent_Obj : trigger.new){
if(!trigger_cntrl.set_a_update.contains(current_Obj.id)){
list_of_new_obj.add(current_obj);
new_map_of_obj.put(current_obj.id,current_obj);
trigger_cntrl.set_a_update.add(current_obj.id);
}
}
if(!list_of_new_obj.isempty()){
trigger_cntrl.afterUpdate(list_of_new_obj,Trigger.Old,
new_map_of_obj,Trigger.Oldmap);
}
}
when BEFORE_DELETE
{
trigger_cntrl.beforedelete(Trigger.New,Trigger.Old,
Trigger.Newmap,Trigger.Oldmap);
}
}
}The Controller Class (Recursion-Safe Handler)
Below is the clean controller class that receives filtered lists/maps and contains dedicated methods for each trigger context.
public class Account_trigger_cntrl {
public static Boolean is_before_insert = true;
public static Boolean is_after_insert = true;
public static Set<Id> set_a_insert = new Set<Id>();
public static Set<Id> set_a_update = new Set<Id>();
public static Set<Id> set_b_update = new Set<Id>();
public static void beforeInsert(list<obj> new_list_obj, list<obj>
old_list_obj, map<id,obj> new_map_obj, map<id,obj> old_map_obj)
{
system.debug('inside before insert trigger');
}
public static void beforeupdate(list<obj> new_list_obj, list<obj>
old_list_obj, map<id,obj> new_map_obj, map<id,obj> old_map_obj)
{
system.debug('inside before update trigger');
}
public static void beforedelete(list<obj> new_list_obj, list<obj>
old_list_obj, map<id,obj> new_map_obj, map<id,obj> old_map_obj)
{
system.debug('inside before delete trigger');
}
public static void afterInsert(list<obj> new_list_obj, list<obj>
old_list_obj, map<id,obj> new_map_obj, map<id,obj> old_map_obj)
{
system.debug('inside after Insert trigger');
}
public static void afterUpdate(list<obj> new_list_obj, list<obj>
old_list_obj, map<id,obj> new_map_obj, map<id,obj> old_map_obj)
{
system.debug('inside after update trigger');
}Why This Framework Works
Prevents Multiple Executions
Records are processed only once using the recursion set.
Supports Heavy Automation
Flows + PB + Apex + Integrations all work without causing trigger loops.
Bulk Friendly
All logic is list-based and executes safely during bulk record operations.
Maintainable & Expandable
Each method corresponds to a specific event, making enhancements easy.
Stable in Production
No more SOQL 101 error No more DML 151 error in trigger. No more
unpredictable recursion.

🎯Final Thoughts
This trigger framework has become our default structure for all Salesforce implementations. It ensures:
Predictable behavior
No unexpected recursion
Clean separation of logic
Full alignment with governor limits
Easy debugging and future enhancements
If your org is scaling or using multiple automation layers, adopting a framework like this is not optional—it’s essential.




Comments