Apex & Asynchronous Processing
Queueable Apex is not a performance button
Asynchronous does not mean faster
A synchronous transaction approaches a limit, so the team moves its work into Queueable Apex. The response is quicker and the original transaction smaller, but the same queries, calculations, writes and callouts still consume platform and downstream capacity. They now happen later, in another transaction, with another failure path.
Define what accepted actually means
Before writing the Queueable class, define the contract at the point of enqueueing. Does the initiating transaction promise only that Salesforce accepted a request, or that a durable business obligation now exists? What status can the user see? Can they continue safely while the job is pending? Which process owns recovery if execution fails after the original transaction has committed?
A returned job ID is useful operational evidence, not business completion. Salesforce documents that System.enqueueJob returns the AsyncApexJob identifier and that job status can be monitored through the Apex Jobs page or by querying AsyncApexJob. A Completed platform job still does not prove that an external order was accepted, an email was meaningful or every intended business record reached the correct state. Define a business outcome alongside the platform outcome.
Capture intent, not accidental state
Although Queueable Apex can carry complex data, prefer a compact request containing stable identifiers, the intended operation, a correlation key and the facts that must remain immutable. Query current data during execution when the job should act on current truth. Persist an explicit command or snapshot when it must honour the state approved at submission. Whichever model you choose, record the relevant version or timestamp and reject work whose preconditions no longer hold. Staleness should be a designed outcome, not a surprise.
Design for at-least-once business effects
Reliable operations assume that a request may be submitted again. A user can click twice, an upstream transaction can retry after an ambiguous response, or an operator can replay failed work. Even if the platform executes one queued job exactly once, the business action may already have occurred elsewhere before a local error made the result look unsuccessful.
Give each logical request an idempotency key and persist its state. Check whether the effect has already completed before changing Salesforce or calling a downstream service. Where the remote API supports an idempotency or correlation key, pass the same stable value. Where it does not, design a lookup or reconciliation step. Retrying without a duplicate policy is not resilience; it is a second chance to create two orders, payments or notifications.
Treat callouts as a distributed transaction
A queueable that implements Database.AllowsCallouts can communicate with another system, but the network boundary prevents one atomic commit across both platforms. The remote system can succeed and the subsequent Salesforce update can fail. Salesforce can commit a local status while the response is lost. Timeout does not tell you whether the other side performed the action.
Model explicit states such as Pending, Sent, Confirmed, Failed and Needs Reconciliation. Store the correlation identifier, attempt count, last outcome and enough sanitised diagnostic context to investigate without exposing secrets. Separate retryable transport failures from rejected business requests. For consequential integrations, provide a reconciliation process that compares authoritative outcomes instead of assuming that another blind callout will make the records agree.
A chain is a workflow
Salesforce supports queueable chaining: a running job can submit a child so that work proceeds sequentially. The documented execution model permits one child from an executing queueable. That makes chaining suitable for ordered stages or bounded partitioning, but the code has now become a workflow engine whether or not the team names it one.
Persist the workflow identity, current stage and stopping condition. Make every stage independently idempotent and safe after a partial success. Bound the number of records and stages, and route permanently failing items to an exception process rather than continuing an invisible loop. If the process needs branching, long waits, human decisions or broad fan-out, evaluate a more explicit orchestration mechanism instead of stretching a queueable chain beyond its understandable shape.
Use finalizers for outcome handling—not magic recovery
The Apex Finalizer interface can attach actions that run at the end of a Queueable execution. It gives teams a useful place to inspect the outcome and centralise telemetry or controlled follow-up. Because the finalizer executes in its own transaction, it can record failure information even when the queueable transaction did not commit its intended changes.
A finalizer does not make an unsafe job safe. It has its own limits and can fail; it cannot infer whether an ambiguous external effect completed; and an automatic retry can repeat a non-idempotent action. Keep finalizer responsibilities narrow: record an outcome, update a durable request, emit an alert or schedule a policy-governed retry. Recovery decisions still need error classification, attempt limits and an owner for exhausted work.
Monitor the promise, not just the job
AsyncApexJob exposes platform execution status and errors, which is necessary for operations. It is not sufficient for a business service. A queue can be healthy while requests wait too long for the customer journey, and every job can complete while downstream rejections accumulate. Dashboards should connect the platform job ID to the business request and correlation key.
Measure queue age, end-to-end completion time, success by outcome, retries, terminal failures and reconciliation backlog. Alert on breached service expectations rather than on every technical exception in isolation. Provide support teams with a safe way to find the request, understand its current state and perform an approved replay. Observability is part of the asynchronous contract, not logging added after the first incident.
Backpressure starts before enqueueing
Salesforce documents shared limits for asynchronous Apex execution, and downstream services often impose their own constraints. Estimate arrival rate, work per request, acceptable delay and downstream capacity. Coalesce redundant requests, partition large workloads deliberately and reject or defer low-priority work when capacity is constrained. Use scheduled or batch-oriented processing when cadence matters more than immediacy. A queue absorbs short bursts; it is not an unlimited reservoir and it does not create throughput from nothing.
Test the boundary and its failure modes
Salesforce's Queueable guidance shows that work enqueued between Test.startTest and Test.stopTest runs during the test, enabling assertions on its results. Use that mechanism, but do not stop at a happy-path record update. Test stale inputs, duplicate logical requests, permission contexts, partial data, callout failures, rejected responses and exhausted retries. Assert business status and side-effect prevention, not only code coverage.
Keep orchestration thin enough that policies can be tested without relying on a long chain to execute in one test. Exercise the queueable entry point separately from the service that performs the work, and verify that the initiating transaction describes the request honestly. Production readiness means proving what happens when the job cannot keep its promise, not merely proving that execute() was invoked.
The JSBC Labs view
Use Queueable Apex when a separate transaction and deferred completion are part of the solution: isolating non-interactive work, sequencing bounded stages, processing after commit or performing a governed callout. Do not use it to disguise an inefficient query, unbounded automation or a user journey whose outcome must be immediate. Fix the workload first, then choose the execution model.
For every queueable, document five things: the acceptance promise, persisted intent, idempotency key, observable outcome and capacity policy. Add explicit reconciliation for cross-system effects and bounded recovery for failures. That discipline turns System.enqueueJob from a convenient escape hatch into a reliable architectural boundary—and makes the system easier to operate when the asynchronous path does exactly what asynchronous systems do: finish later, fail separately and demand evidence.