This content originally appeared on DEV Community and was authored by Rohit Maharashi
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 System.enqueueJob() call 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 State object
• Work in both synchronous and asynchronous contexts
• Be reusable across object trigger handlers
The final flow will look like this:
Trigger.afterInsert() ➔ A ➔ B ➔ C ➔ C1 ➔ C2 ➔ D ➔ E
⸻
Core Components
- BaseChainedQueueable
An abstract base class to support chaining jobs one after another.
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();
}
⸻
- 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);
}
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.
⸻
- State Class
A generic key-value store to pass contextual data between jobs:
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);
}
}
⸻
- QueueableOrchestrator
Handles safe job submission based on execution 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);
}
}
}
⸻
- JobQueueManager
Central place to build and connect the job chain:
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);
}
}
⸻
- 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 defined sequence, even with sub-jobs.
• Governor Safe — One enqueueJob() per async context.
• Modular and Testable — Each job is self-contained and reusable.
• Reusable Architecture — Extendable to any object’s trigger.
• Input-Aware — Jobs receive input from a shared State.
⸻
Closing the Loop
This pattern abstracts away common Salesforce platform limitations, providing a clean, object-oriented framework for managing chained asynchronous logic from Apex triggers.
Whether you’re scaling post-processing across multiple SObjects or introducing conditional sub-flows, this solution lets you build predictably, safely, and cleanly.
This content originally appeared on DEV Community and was authored by Rohit Maharashi