Skip to main content

Master Salesforce Queueable Apex: Chaining, Finalizers, and Cursors

๐Ÿ’ฌ In plain words:
Queueable Apex is Salesforce's modern asynchronous worker. Unlike older methods, it accepts complex objects as parameters and returns a trackable Job ID. Best of all, it allows for chaining—one job can trigger the next, passing the baton like a relay race. By attaching a Transaction Finalizer, you build a safety net that guarantees specific code will run (for logging or retrying) even if the main job crashes.
๐Ÿ“Œ The Order Fulfillment Example:
Imagine an order ships. Job 1 calls the courier API. Once complete, it enqueues Job 2 to update the inventory system. Job 2 then enqueues Job 3 to email the invoice. This is an organized relay race where every runner gets a fresh set of governor limits. If Job 2 crashes due to a limit exception, its Transaction Finalizer catches the failure, logs it, and re-queues the job to try again.
๐ŸŽฌ Real-Life Application: The Relay That Never Drops the Baton

A company needs to process billing: first, call a payment API; second, transform the response data; third, save the results. These steps must happen in exact order.

The Old/Bad Way: Using three @future methods fired sequentially.
Why it is bad: @future methods do not return a Job ID, they cannot be explicitly chained, and they do not guarantee execution order. Step two might run before step one finishes. If something fails, it vanishes silently.

The New/Good Way:
  • Create one Queueable class for each step.
  • Once the first execute() method finishes its work, use System.enqueueJob() to trigger the next step.
  • Pass complex data types directly between classes, though remember to re-query Salesforce records to get fresh data.
  • Attach a Transaction Finalizer to handle errors gracefully and implement retries.
The Payoff: Perfectly ordered steps, trackable Job IDs, and guaranteed error handling if a limit exception occurs.
๐Ÿง  Memory Aid: The Relay Race
Pass objects in, get a Job ID out. Each runner passes the baton to the next. Finalizers are the coaches who catch dropped batons and put the runner back on the track.

Core Concept: How Queueable Works

Queueable is the standard tool for modern async operations in Salesforce. Because it relies on standard object-oriented classes, it can hold complex state and accept full sObjects or custom types via its constructor.

  • Use System.enqueueJob() to place it in the queue. You get a Job ID back immediately to monitor its progress.
  • Need to make external API calls? Just implement the Database.AllowsCallouts interface.
  • You can add a delay! System.enqueueJob() now accepts a delay parameter ranging from 0 to 10 minutes.
  • Use AsyncOptions to control the chain depth (using MaximumQueueableStackDepth) or to prevent duplicate jobs from firing based on a unique signature.

The Power of Transaction Finalizers

Finalizers are the ultimate safety net. By calling System.attachFinalizer() inside your Queueable, you register a callback that executes immediately after the Queueable finishes. This runs even if the job crashes due to an uncatchable limit exception. It acts as a platform-level try/finally block, making it the perfect place to orchestrate guaranteed retries and write error logs.

Salesforce Queueable Apex Chaining and Finalizers
๐Ÿงญ 360 Card: Queueable Apex Summary
  • Rule: Use Queueable for asynchronous tasks requiring execution sequence ("do this, then that").
  • Gain: Accepts full sObjects, provides a trackable Job ID, supports chaining, and allows finalizers for guaranteed post-job execution.
  • Price: You can generally only chain one child job per execute() method. If you need to "fan-out" and do lots of things at once, organize by data, not by launching dozens of jobs.
  • Limits: You can enqueue up to 50 jobs in a single synchronous transaction. Developer/Trial orgs restrict chain depth to 5; production orgs are effectively unbounded.
  • Mirror vs Batch: Batch Apex automatically chops massive volumes for you. Queueable is better for distinct workflow steps.

Queueable & Finalizer Code Example

// 1. Queueable class allowing callouts and implementing Finalizer
public class SyncJob implements Queueable, Database.AllowsCallouts, Finalizer {
    private Integer attempt;

    public SyncJob(Integer attempt) { 
        this.attempt = attempt; 
    }

    public void execute(QueueableContext ctx) {
        // 2. Attach the finalizer BEFORE doing the work
        System.attachFinalizer(this);
        
        // Execute API callout or heavy database work here
    }

    // 3. This executes after the Queueable ends (even if an exception occurred)
    public void execute(FinalizerContext fc) {
        // 4. Retry logic: Re-queue if it failed (up to 3 attempts)
        if (fc.getResult() == ParentJobResult.UNHANDLED_EXCEPTION && attempt < 3) {
            System.enqueueJob(new SyncJob(attempt + 1));
        }
        // Always write a log record so nothing is silently lost
    }
}
  

Core Q&A

Q: When does Queueable beat Batch Apex, and how do Finalizers change your retry architecture?

๐ŸŽฏ Say this first: Queueable wins for event-based workflows, step-by-step chaining, and passing complex parameters. Finalizers provide a guaranteed landing spot after a success or a crash, which is exactly where your retry logic and logging should live.

A: Queueable is best for single-pass tasks with rich parameters—like making an API callout after a record saves, or generating a PDF document. Batch Apex has overhead (start, execute, finish methods) that slows these simple tasks down unnecessarily.

Finalizers give your retries teeth. The finalizer context receives the actual result of the job. If the job died because of a LimitException (which standard try/catch blocks cannot handle), the finalizer still fires. It can evaluate the failure, update a retry counter, and re-enqueue the job safely.

Follow-ups (Scenario-Based)

Q1: What are the chaining depth rules, and how do you process an unbounded queue with them?

A1: In production environments, chain depth is effectively unbounded (one child enqueue per execute). However, Developer and Trial editions cap the depth at 5 chained jobs. You can also manually cap it using the stack-depth parameter on enqueueJob.

To process an unbounded queue: Design the job so each link processes one chunk of data from a custom "work queue" object. It marks that record as complete, and then enqueues the next Queueable only if work remains. This creates a self-draining chain that stops naturally. To make it bulletproof, add a scheduled "sweeper" job to restart the chain if it ever stalls, working alongside your Finalizer retries.

⚠ INTERVIEW TRAP: The Fan-Out Mistake
Q2: One transaction needs to enqueue 60 Queueables. What limit do you hit and what is the redesign?
A2: You will hit the strict limit of 50 System.enqueueJob() calls per synchronous transaction. Furthermore, launching 60 sibling jobs at once is a design smell.

The Fix: Fan-out via data, not via jobs. You have three better options:
  • Write 60 records to a custom staging object, then launch one Queueable "dispatcher" that processes them recursively.
  • Use Batch Apex, setting the scope size to the appropriate unit limit.
  • Publish 60 Platform Events and let an asynchronous subscriber handle the consumption sequentially.
Relying on job fan-outs destroys processing order and clutters your AsyncApexJob logs. Fan-out via data preserves both.

Q: (Compare) Queueable chaining vs Batch Apex — both process big work in pieces; how do you choose?

A: Choose Batch when the task is "run this same operation over millions of rows." The platform handles the massive chunking for you. Choose Queueable chaining when the work represents different sequence steps (e.g., Step 1: API Callout, Step 2: Transform Data, Step 3: Save to Database) or when a step relies heavily on the outcome of the previous step. Volume problem = Batch. Workflow problem = Queueable.

Q: What are Apex Cursors, and when do they beat Batch Apex?

A: Apex Cursors went Generally Available (GA) in the Spring '26 release. Instead of loading records into memory, Database.getCursor(soql) stores a pointer to the query result.

  • You use fetch(position, n) to pull slices of records (up to 50 million rows per cursor).
  • Cursors are stateless. You can navigate forward and backward through the dataset.
  • They pair perfectly with Queueable: process a slice, then chain the next Queueable job passing the new position.
  • If a transient failure occurs, it throws a System.TransientCursorException, letting you retry just that specific slice instead of the whole run.
When to use Cursors: When you need extreme Queueable-style control, custom chunk sizes, or backward navigation through data.
When Batch still wins: If you just need classic scheduled chunking with built-in retries and a convenient finish() hook. Thanks to Cursors, high data volume alone no longer forces you into using Batch Apex.

Q: Two quick "are you current" Apex checks interviewers use?

A: 1. The Null Coalescing Operator: Introduced in Spring '24, a ?? b returns b whenever a is null. It elegantly replaces messy ternary operators.
2. Salesforce Functions: They have been retired! If an interviewer asks where heavy, elastic compute tasks should live now, the correct answer is Heroku (or another external cloud service accessed via callout)—not Salesforce Functions.