🚀 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.
Top comments (0)