9.2 Queueable Apex

Key Takeaways

  • Implement the Queueable interface and enqueue with System.enqueueJob; execute(QueueableContext) holds the job logic
  • Unlike @future, Queueable can accept complex types including sObjects in the constructor and supports job Ids
  • You can chain another queueable from execute (with platform limits on depth/stacking); this enables multi-step async workflows
  • Implement Database.AllowsCallouts on the queueable class when the job performs HTTP callouts
  • Prefer Queueable over future for flexible state, chaining, and monitoring; prefer Batch when processing very large data volumes in chunks
Last updated: August 2026

9.2 Queueable Apex

Quick Answer: Queueable Apex lets you implement the Queueable interface, pass rich state (including sObjects) through a constructor, and run work asynchronously via System.enqueueJob. You get a job Id, optional callouts (Database.AllowsCallouts), and limited chaining—making Queueable the default upgrade path over @future for most deferred Apex.

Platform Developer I expects you to choose the right async tool. Queueable sits between simple @future methods and heavy Batch Apex: flexible state and chaining without the full start/execute/finish batch machinery.

The Queueable Interface

public class ContactEnrichmentJob implements Queueable {
    private List<Contact> contacts;

    public ContactEnrichmentJob(List<Contact> contacts) {
        this.contacts = contacts;
    }

    public void execute(QueueableContext context) {
        // Optional: Id jobId = context.getJobId();
        for (Contact c : contacts) {
            c.Description = 'Enriched async';
        }
        update contacts;
    }
}

Enqueue from a trigger, Flow-invoked Apex, controller, or another async finish method:

Id jobId = System.enqueueJob(new ContactEnrichmentJob(Trigger.new));
// Persist jobId if you need user-visible status or custom monitoring

System.enqueueJob returns an AsyncApexJob Id (as an Id value). That alone is a major advantage over @future, which does not give you the same first-class enqueue handle in application code patterns emphasized on the exam.

Why sObject Parameters Matter

@future forbids sObject parameters. Queueable does not—you pass state through the constructor (or instance fields set before enqueue). That enables patterns such as:

  • Passing a filtered list of Trigger.new records (careful: still prefer Ids when records may change before the job runs)
  • Passing Maps, custom Apex types, and configuration flags
  • Building multi-step jobs with intermediate collections

Design caution: Enqueued jobs serialize their state. Passing huge heaps of records can hit async heap limits. For very large volumes, Batch Apex with a QueryLocator is safer than stuffing thousands of sObjects into one queueable.

When data freshness matters more than the in-memory snapshot, still pass Ids and re-query inside execute—same best practice as future methods, but now optional rather than forced by the language.

Chaining Queueable Jobs

From inside execute, you may enqueue another queueable:

public class StepOneJob implements Queueable {
    public void execute(QueueableContext context) {
        // ... do step one work ...
        System.enqueueJob(new StepTwoJob(/* state */));
    }
}

Chaining supports multi-phase workflows: validate → callout → update → notify. Exam points:

  • Chaining is a supported Queueable capability that @future lacks (no future-from-future).
  • There are platform limits on how queueable jobs stack and how deep chains can go in a transaction path; do not design infinite recursive enqueue loops.
  • Each chained job is a new async transaction with its own governors.
  • Failures in step two do not automatically roll back step one (separate transactions)—design idempotency and compensating logic when business requires it.

Depth limit awareness: In a single synchronous transaction you also face limits on how many jobs you can enqueue. Prefer one job that processes a bulk collection over enqueuing one job per record.

Callouts with Queueable

Implement Database.AllowsCallouts (marker interface) on the class:

public class CaseWebhookJob implements Queueable, Database.AllowsCallouts {
    private Set<Id> caseIds;

    public CaseWebhookJob(Set<Id> caseIds) {
        this.caseIds = caseIds;
    }

    public void execute(QueueableContext context) {
        List<Case> cases = [
            SELECT Id, CaseNumber, Subject FROM Case WHERE Id IN :caseIds
        ];
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Service_NC/hooks/cases');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(cases));
        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() >= 400) {
            // log, retry via chain, or throw to mark job failed
        }
    }
}

Compare to @future(callout=true): Queueable’s interface-based approach is clearer when combined with non-primitive state and chaining (for example, callout job chains to a “persist response” job).

Job IDs and Monitoring

Id jobId = System.enqueueJob(new ContactEnrichmentJob(contacts));

Use the job Id to:

  • Query AsyncApexJob for status (Queued, Processing, Completed, Failed, Aborted)
  • Show admins or custom UIs that work was submitted
  • Correlate debug logs and error emails with a specific enqueue
AsyncApexJob job = [
    SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems
    FROM AsyncApexJob
    WHERE Id = :jobId
];

Batch jobs expose more “items processed” fields because they run in chunks; queueable jobs are typically single-execute units, but AsyncApexJob still reports lifecycle status.

Comparison: Future vs Queueable vs Batch

Capability@futureQueueableBatch
ImplementationAnnotation on static void methodQueueable class + enqueueJobDatabase.Batchable + Database.executeBatch
sObject / complex stateNoYes (constructor fields)Yes (instance state; Stateful optional)
Job Id to callerNot the primary patternYes (enqueueJob)Yes (executeBatch)
ChainingNo future→futureYes (enqueue from execute)Yes (often from finish)
Callouts@future(callout=true)Database.AllowsCalloutsDatabase.AllowsCallouts
Best scaleSmall/medium deferred unitsMedium deferred units, workflowsLarge data volumes in scopes
Chunked processingNo built-inManual / chainBuilt-in start/execute/finish
Scheduled directlyNoNo (schedule a Schedulable that enqueues)Often launched from Schedulable

Exam heuristics:

  • “Process millions of records nightly” → Batch (possibly scheduled).
  • “After trigger, call out with related records and maybe chain a follow-up” → Queueable.
  • “Static void, Set of Ids only, mixed DML split” → @future still acceptable; Queueable also works if options allow it.

Common Patterns for Post-Commit Async Work

Pattern A — Trigger enqueues one bulk job

// Trigger handler after insert
if (!Trigger.new.isEmpty()) {
    System.enqueueJob(new CaseWebhookJob(
        new Map<Id, Case>(Trigger.new).keySet()
    ));
}

One enqueue for the whole trigger batch keeps you inside async enqueue limits.

Pattern B — Service layer decides sync vs async

Business services expose sync methods for small paths and queueable wrappers for integrations. Controllers call the service; the service chooses Queueable when callouts or long work would risk the interactive transaction.

Pattern C — Queueable after partial DML success

When using Database.insert(records, false), collect successful Ids and enqueue enrichment only for those Ids so failed rows are not sent to external systems.

Pattern D — Chained callout then DML

Some designs: Job1 performs callout only; Job2 performs DML based on stored results or re-query. Splitting can simplify transaction rules and retries.

Pattern E — Replace @future incrementally

Refactoring legacy @future methods into Queueable classes improves testability (injectable state), monitoring (job Id), and future chaining needs without changing when the work runs (still async post-commit from the caller’s perspective).

Testing Queueable Apex

Same start/stop discipline as future methods:

@IsTest
static void enrichmentRuns() {
    Contact c = new Contact(LastName = 'Async');
    insert c;

    Test.startTest();
    System.enqueueJob(new ContactEnrichmentJob(
        [SELECT Id, Description FROM Contact WHERE Id = :c.Id]
    ));
    Test.stopTest();

    Contact result = [SELECT Description FROM Contact WHERE Id = :c.Id];
    System.assertEquals('Enriched async', result.Description);
}

For callout queueables, register HttpCalloutMock before stopTest forces execution. Assert both DML side effects and that the mock received the expected request when relevant.

Choosing Queueable on Scenario Questions

Pick Queueable when the stem mentions any of: non-primitive/sObject inputs, System.enqueueJob, job Id monitoring, chaining steps, or “more flexible than future.” Pick Batch when volume, QueryLocator, or per-scope governors dominate. Pick @future when the stem is locked to annotation syntax, void static methods, or explicit future limits.

Queueable is the workhorse of modern async Apex on the exam—and in real orgs—once you outgrow Id-only future methods but do not yet need batch-scale chunking.

Test Your Knowledge

How do you start a Queueable job and obtain its job Id?

A
B
C
D
Test Your Knowledge

Which statement correctly contrasts Queueable with @future?

A
B
C
D
Test Your Knowledge

A queueable class must perform an HTTP callout in execute. What should the developer do?

A
B
C
D