⚙️ Scalable and Ordered Queueable Execution from Triggers in Salesforce



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

  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();
}

  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');
    }
}
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.

  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);
    }
}

  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);
        }
    }
}

  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);
    }
}

  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);
    }
}

✅ 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