DEV Community

Rohit Maharashi
Rohit Maharashi

Posted on

⚙️ 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 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

  1. 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(); } 
Enter fullscreen mode Exit fullscreen mode

  1. 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'); } } 
Enter fullscreen mode Exit fullscreen mode
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); } } 
Enter fullscreen mode Exit fullscreen mode
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'); } } 
Enter fullscreen mode Exit fullscreen mode

Repeat similarly for JobB, JobD, and JobE.

  1. 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); } } 
Enter fullscreen mode Exit fullscreen mode

  1. 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); } } } 
Enter fullscreen mode Exit fullscreen mode

  1. 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); } } 
Enter fullscreen mode Exit fullscreen mode

  1. 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); } } 
Enter fullscreen mode Exit fullscreen mode

✅ 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)