Scalable and Ordered Queueable Execution from Triggers in Salesforce
🚀 Problem Statement
Scalable and Ordered Queueable Execution from Triggers in Salesforce
🚀 Problem Statement
In Salesforce, Apex triggers often need to perform post-commit processing or heavy logic that exceeds synchronous limits. Queueable Apex jobs are the go-to solution for such asynchronous processing. However, there’s a critical platform limitation:
When a transaction is already running asynchronously (e.g., from another Queueable/Future/Batch), only one
Queueablecall is allowed.
This makes it difficult to:
- Enqueue multiple jobs from a trigger
- Maintain ordered execution of those jobs
- Support nested internal chaining (e.g., job C triggers C1 and C2)
- Handle complex input data, possibly from the trigger state
- Build a framework that works for any object
🛠️ Design Intent
We aim to build a generic, scalable, and governor-safe Apex framework to:
- Enqueue multiple queueable jobs in order
- Allow jobs to chain additional sub-jobs internally
- Support input injection from a
Stateobject - Work in both synchronous and asynchronous contexts
- Be reusable across object trigger handlers
The final flow will look like :
Trigger.afterInsert/Update() ➔ A ➔ B ➔ C ➔ C1 ➔ C2 ➔ D ➔ E
🔧 Core Components
1. BaseChainedQueueable
An abstract base class that supports chaining:
public abstract class BaseChainedQueueable implements Queueable {
protected BaseChainedQueueable nextJob;
public void setNext(BaseChainedQueueable nextJob) {
this.nextJob = nextJob;
}
public void execute(QueueableContext context) {
run();
if (nextJob != null) {
System.enqueueJob(nextJob);
}
}
public abstract void run();
}
2. Sample Job Classes
public class JobA extends BaseChainedQueueable {
private List<Account> accounts;
public JobA(List<Account> accounts) { this.accounts = accounts; }
public override void run() { System.debug('Running A'); }
}
public class JobC extends BaseChainedQueueable {
private List<Contact> contacts;
public JobC(List<Contact> contacts) { this.contacts = contacts; }
public override void run() {
System.debug('Running C');
JobC1 c1 = new JobC1();
JobC2 c2 = new JobC2();
c1.setNext(c2);
if (nextJob != null) {
c2.setNext(nextJob);
//Explicity blank out nextJob to ensure JobC doesn't enqueue both JobD and JobC1
nextJob = null;
}
System.enqueueJob(c1);
}
}
public class JobC1 extends BaseChainedQueueable {
public override void run() { System.debug('Running C1'); }
}
public class JobC2 extends BaseChainedQueueable {
public override void run() { System.debug('Running C2'); }
}
Repeat similarly for JobB, JobD, and JobE.
3. State Class
Holds the intermediate state from the trigger:
public class State {
private Map<String, Object> data = new Map<String, Object>();
public void put(String key, Object value) { data.put(key, value); }
public Object get(String key) { return data.get(key); }
}
4. QueueableOrchestrator
Safely enqueues the job chain entry point depending on context:
public class QueueableOrchestrator {
public static void run(BaseChainedQueueable firstJob) {
if (AsyncUtils.isAsync()) {
System.enqueueJob(new ChainedStarter(firstJob));
} else {
System.enqueueJob(firstJob);
}
}
private class ChainedStarter implements Queueable {
private BaseChainedQueueable job;
public ChainedStarter(BaseChainedQueueable job) { this.job = job; }
public void execute(QueueableContext context) {
System.enqueueJob(job);
}
}
}
5. JobQueueManager
Central place to build and chain all jobs:
public class JobQueueManager {
public static void runChainedJobsFromState(State state) {
List<Account> accounts = (List<Account>) state.get('accounts');
List<Contact> contacts = (List<Contact>) state.get('contacts');
List<Opportunity> opps = (List<Opportunity>) state.get('opps');
JobA a = new JobA(accounts);
JobB b = new JobB(opps);
JobC c = new JobC(contacts);
JobD d = new JobD();
JobE e = new JobE();
a.setNext(b); b.setNext(c); c.setNext(d); d.setNext(e);
QueueableOrchestrator.run(a);
}
}
6. Trigger Handler Integration
public class AccountTriggerHandler extends TriggerHandler {
public override void afterInsert() {
State state = new State();
state.put('accounts', (List<Account>) Trigger.new);
List<Id> accIds = new Map<Id, Account>(Trigger.new).keySet();
state.put('contacts', ContactSelector.getByAccountIds(accIds));
state.put('opps', OpportunitySelector.getByAccountIds(accIds));
JobQueueManager.runChainedJobsFromState(state);
}
}
✅ Benefits Recap
- ⏱ Fully Ordered Execution: Jobs execute in strict sequence, even with internal chains.
- 🚪 Governor Safe: Only one
enqueueJob()per async context. - 💪 Modular: Each job is encapsulated and testable.
- ✨ Reusable: Extendable across any object’s trigger.
- ⚖️ Input-Aware: Jobs can accept multiple inputs from a central
Stateobject.
📚 Closing the Loop
This design abstracts away common platform limitations while providing a clean, object-oriented framework for orchestrating queueables from triggers. Whether you’re scaling post-processing across multiple objects or introducing internal chains, this pattern allows you to do so predictably, safely, and cleanly.
Use this as your go-to template when you need multi-step asynchronous workflows driven by trigger events.
메타데이터
- post_id
- 421bfb3aa19c
- slug
- scalable-and-ordered-queueable-execution-from-triggers-in-salesforce-421bfb3aa19c
- url
- https://medium.com/@rmaharashi/scalable-and-ordered-queueable-execution-from-triggers-in-salesforce-421bfb3aa19c
- canonical_url
- https://medium.com/@rmaharashi/scalable-and-ordered-queueable-execution-from-triggers-in-salesforce-421bfb3aa19c
- author_url
- https://medium.com/@rmaharashi
- status
- ok
- fetched_at
- 2026-08-19 05:11:59