Asynchronous Apex Patterns
Based on Real Implementation Experience: These patterns come from high-volume integrations, data migrations, and long-running processes where synchronous Apex or Flow is not sufficient.
Overview
Asynchronous Apex APIs—Batch, Queueable, Scheduled, and @future—allow you to:
- Process large data volumes without blocking user transactions.
- Perform callouts after DML.
- Run logic on a schedule or in response to events.
This document describes when to use each mechanism and how to structure code so that async jobs are testable, observable, and safe.
Prerequisites
- Required Knowledge:
- Core Apex language features.
- Synchronous trigger and service patterns (see
apex-patterns.md).
- Recommended Reading:
When to Use
Use Async Apex When
- Processing large datasets (tens or hundreds of thousands of records).
- Making callouts after DML, especially from triggers or record-triggered Flows.
- Running periodic maintenance jobs (nightly cleanup, recalculation, sync).
- Chaining multi-step workflows that would hit limits in a single transaction.
Avoid Async Apex When
- You only have a few records and simple logic (synchronous is enough).
- You need immediate user feedback in the same transaction.
- The pattern can be satisfied with record-triggered Flows with async paths and is easier to maintain declaratively.
Core Concepts
Transaction Boundaries
Each async job runs in its own transaction with its own governor limits. This allows you to:
- Reset limits after heavy synchronous work.
- Break work into multiple independent units.
Idempotency
Async jobs should be idempotent:
- Safe to retry without corrupting data.
- Use external Ids or status flags to avoid double-processing.
Visibility and Monitoring
Use:
AsyncApexJobqueries for status.- Structured logging (see
error-handling-and-logging.md). - Platform Events where appropriate to signal completion.
Patterns and Examples
Pattern 1: Queueable for Post-Commit Work
Intent: Perform work that should happen after the main transaction commits (e.g., callouts, notifications).
Structure:
- Trigger/Flow enqueues a Queueable with the required context.
- Queueable:
- Queries records.
- Performs callouts or DML.
- Logs results and errors.
See Queueable Examples for code.
Pattern 2: Batch Apex for Large Data Volumes
Intent: Process hundreds of thousands or millions of records safely.
Structure:
- Implement
Database.Batchable<SObject>. - Use a selective query in
start. - Perform processing in
executewith bulk-safe DML. - Summarize results in
finish.
See Batch Examples and apex-batch-template.md.
Pattern 3: Scheduled Apex for Time-Based Jobs
Intent: Run jobs at fixed times (nightly, weekly, hourly).
Structure:
- Implement
Schedulable. - In
execute, enqueue Queueable or start Batch job. - Use the UI or cron expression to schedule.
Edge Cases and Limitations
- Org-wide limits on concurrent async jobs (Batch, Queueable, future).
- Chaining too many jobs can starve other processes.
- Async jobs can fail silently if not monitored; always log and alert.
- Async work on records that are frequently updated can cause locking conflicts; coordinate with locking strategies.
Related Patterns
- Locking and Concurrency Strategies
- Error Handling and Logging Framework
- Change Data Capture Patterns
Q&A
Q: When should I choose Batch Apex over Queueable?
A: Use Batch when you need to process very large datasets, need chunking with per-batch context, or want to track progress across many records. Use Queueable for lighter workloads, callouts, and chaining a few jobs where you control the scope.
Q: How many Queueable jobs can I chain?
A: Salesforce enforces limits on concurrent async jobs and job chaining. As a good practice, keep chains short and intentional, and consider Batch or multiple entry points if you need deeper workflows. Always consult the latest governor limits documentation.
Q: How do I test asynchronous Apex?
A: Use Test.startTest() and Test.stopTest() to ensure async jobs run within the test context. Assert on the final state of records, logs, or AsyncApexJob records. For callouts, use Test.setMock() with HttpCalloutMock implementations.