Asynchronous Apex: Future, Queueable, Batch, Schedulable
📖 Concept
Asynchronous Apex allows you to run code outside the current transaction, giving you higher governor limits and the ability to perform long-running operations. Understanding when to use each async pattern is a key senior-level skill.
The four async patterns:
@future Methods — Simplest async pattern
- Static method with @future annotation
- Runs in its own transaction with higher limits
- Cannot chain (no future from future)
- Cannot track execution status
- Parameters must be primitive types (no SObjects)
- Use for: simple callouts, quick fire-and-forget operations
Queueable Apex — Enhanced @future
- Implements Queueable interface
- CAN use non-primitive parameters (SObjects, custom types)
- CAN chain jobs (one Queueable can enqueue another)
- CAN track via AsyncApexJob
- Returns a Job ID for monitoring
- Use for: complex async operations, sequenced processing
Batch Apex — For processing large data volumes
- Implements Database.Batchable
- Processes records in configurable chunks (scope size, default 200)
- Each chunk gets its own governor limits
- Three phases: start() → execute() × N → finish()
- Can process millions of records
- Use for: data cleanup, mass updates, nightly jobs
- Implements Database.Batchable
Schedulable Apex — Cron-based scheduling
- Implements Schedulable interface
- Runs at specified times (cron expression)
- Often combined with Batch Apex
- Use for: nightly data processing, periodic cleanup, scheduled reports
Comparison:
Pattern | Max Params | Chaining | Monitoring | Use Case
-------------|-------------|-----------|------------|-------------------
@future | Primitives | No | Limited | Simple callouts
Queueable | Any type | Yes (1) | Job ID | Complex async
Batch | Query/List | Chain in | Job ID | Large volumes
| | finish() | |
Schedulable | N/A | Starts | Job ID | Recurring jobs
| | other jobs| |
💻 Code Example
1// Asynchronous Apex — All Four Patterns23// 1. @future — Simple async execution4public class FutureExample {56 // Basic @future method7 @future8 public static void updateAccountDescription(Set<Id> accountIds) {9 List<Account> accounts = [10 SELECT Id, Description FROM Account WHERE Id IN :accountIds11 ];12 for (Account acc : accounts) {13 acc.Description = 'Updated async at ' + Datetime.now();14 }15 update accounts;16 }1718 // @future with callout19 @future(callout=true)20 public static void syncToExternalSystem(Set<Id> accountIds) {21 List<Account> accounts = [22 SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds23 ];2425 HttpRequest req = new HttpRequest();26 req.setEndpoint('callout:ExternalCRM/api/accounts');27 req.setMethod('POST');28 req.setHeader('Content-Type', 'application/json');29 req.setBody(JSON.serialize(accounts));3031 HttpResponse res = new Http().send(req);32 if (res.getStatusCode() != 200) {33 // Log error — can't throw exception back to caller34 System.debug(LoggingLevel.ERROR, 'Sync failed: ' + res.getBody());35 }36 }37}3839// 2. Queueable — Advanced async with chaining40public class QueueableExample implements Queueable, Database.AllowsCallouts {4142 private List<Account> accounts;43 private Integer chainDepth;4445 public QueueableExample(List<Account> accounts, Integer depth) {46 this.accounts = accounts;47 this.chainDepth = depth;48 }4950 public void execute(QueueableContext context) {51 // Process current batch52 for (Account acc : accounts) {53 acc.Description = 'Processed by Queueable - Depth: ' + chainDepth;54 }55 update accounts;5657 // Chain to next job if more work remains58 if (chainDepth < 5) { // Max 5 chains in production59 List<Account> nextBatch = [60 SELECT Id, Description FROM Account61 WHERE Description = null62 LIMIT 20063 ];6465 if (!nextBatch.isEmpty()) {66 System.enqueueJob(67 new QueueableExample(nextBatch, chainDepth + 1)68 );69 }70 }71 }72}7374// Usage:75// Id jobId = System.enqueueJob(new QueueableExample(accounts, 1));76// AsyncApexJob job = [SELECT Status FROM AsyncApexJob WHERE Id = :jobId];7778// 3. Batch Apex — Large volume processing79public class AccountCleanupBatch implements80 Database.Batchable<SObject>,81 Database.Stateful,82 Database.AllowsCallouts {8384 // Stateful — instance variables persist across execute() calls85 private Integer recordsProcessed = 0;86 private Integer errorCount = 0;87 private List<String> errors = new List<String>();8889 // START — Define the data to process90 public Database.QueryLocator start(Database.BatchableContext bc) {91 return Database.getQueryLocator([92 SELECT Id, Name, Industry, LastModifiedDate,93 AnnualRevenue, Description94 FROM Account95 WHERE LastModifiedDate < LAST_N_DAYS:36596 AND Industry = null97 ]);98 // Can return up to 50 MILLION records99 }100101 // EXECUTE — Process each chunk (default 200 records)102 public void execute(Database.BatchableContext bc, List<Account> scope) {103 List<Account> toUpdate = new List<Account>();104105 for (Account acc : scope) {106 try {107 acc.Industry = 'Other';108 acc.Description = 'Auto-classified on ' + Date.today();109 toUpdate.add(acc);110 } catch (Exception e) {111 errorCount++;112 errors.add(acc.Name + ': ' + e.getMessage());113 }114 }115116 Database.SaveResult[] results = Database.update(toUpdate, false);117 for (Database.SaveResult sr : results) {118 if (sr.isSuccess()) {119 recordsProcessed++;120 } else {121 errorCount++;122 }123 }124 }125126 // FINISH — Post-processing (send notification, chain next batch)127 public void finish(Database.BatchableContext bc) {128 // Send completion notification129 Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();130 mail.setToAddresses(new List<String>{'admin@company.com'});131 mail.setSubject('Account Cleanup Batch Complete');132 mail.setPlainTextBody(133 'Records processed: ' + recordsProcessed + '\n' +134 'Errors: ' + errorCount + '\n' +135 'Error details: ' + String.join(errors, '\n')136 );137 Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{mail});138139 // Chain another batch if needed140 // Database.executeBatch(new NextBatchJob(), 200);141 }142}143144// Execute batch:145// Id batchId = Database.executeBatch(new AccountCleanupBatch(), 200);146// // Monitor:147// AsyncApexJob job = [148// SELECT Status, NumberOfErrors, JobItemsProcessed, TotalJobItems149// FROM AsyncApexJob WHERE Id = :batchId150// ];151152// 4. Schedulable — Run at specific times153public class WeeklyCleanupScheduler implements Schedulable {154155 public void execute(SchedulableContext sc) {156 // Launch a batch job157 Database.executeBatch(new AccountCleanupBatch(), 200);158 }159}160161// Schedule via code:162// String cronExp = '0 0 2 ? * SAT'; // Every Saturday at 2 AM163// System.schedule('Weekly Account Cleanup', cronExp, new WeeklyCleanupScheduler());164165// Schedule via Anonymous Apex:166// WeeklyCleanupScheduler scheduler = new WeeklyCleanupScheduler();167// String sch = '0 0 0 * * ?'; // Every day at midnight168// System.schedule('Daily Cleanup', sch, scheduler);169170// CRON Expression format:171// Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year172// 0 0 2 ? * SAT = Every Saturday at 2:00 AM
🏋️ Practice Exercise
Async Apex Practice:
- Write a @future method that makes a callout to an external API and logs the response
- Build a Queueable job that processes records in batches with chaining (max 5 chains)
- Create a Batch Apex job that cleans up old records across 3 objects (use Database.Stateful to track counts)
- Write a Schedulable class that triggers a Batch job every Sunday at 3 AM
- Implement a Queueable job with error handling that retries failed operations
- Create a batch job that makes callouts for each record (requires Database.AllowsCallouts)
- Write a test class for a Batch Apex job using Test.startTest() and Test.stopTest()
- Build a monitoring dashboard that shows the status of all AsyncApexJobs
- Implement a chained processing pipeline: Schedulable → Batch → Queueable → @future
- Design a retry framework using Platform Events and Queueable Apex
⚠️ Common Mistakes
@future methods cannot accept SObject parameters — only primitives (Integer, String, Set
, List ). Pass IDs and re-query inside the method Chaining Queueable from Queueable is limited to 1 child job in production (but unlimited in tests). For complex chains, use Batch Apex or Platform Events
Not using Database.Stateful in Batch — without it, instance variables are reset between execute() chunks. Your counters will be wrong
Testing async code without Test.startTest()/Test.stopTest() — async code doesn't execute in tests without these boundaries
Scheduling too many concurrent Batch jobs — the limit is 5 concurrent batch jobs. Additional jobs queue but may delay processing
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Asynchronous Apex: Future, Queueable, Batch, Schedulable. Login to unlock this feature.