Sometimes your code runs too long or makes too many callouts for synchronous execution. Salesforce provides three mechanisms for asynchronous processing: @future methods, Queueable Apex, and Batch Apex. Each has different use cases.
@future is the simplest async approach. It executes in the future, separate from the request that scheduled it.
public class NotificationService {
@future(callout=true)
public static void sendEmailAsync(Set<Id> userIds, String subject, String body) {
List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
for (Id userId : userIds) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setTargetObjectId(userId);
mail.setSubject(subject);
mail.setPlainTextBody(body);
mails.add(mail);
}
Messaging.sendEmail(mails);
}
}
// Called from trigger or class
trigger OrderTrigger on Order (after insert) {
Set<Id> accountOwnerIds = new Set<Id>();
for (Order ord : Trigger.new) {
// Collect owner IDs
}
NotificationService.sendEmailAsync(accountOwnerIds, 'New Order', 'Order placed successfully');
}
Queueable Apex is more powerful than @future. It allows passing objects, returns a Job ID for tracking, and supports chaining.
public class CalculateAccountMetrics implements Queueable {
private List<Account> accounts;
private String nextStep;
public CalculateAccountMetrics(List<Account> accounts, String nextStep) {
this.accounts = accounts;
this.nextStep = nextStep;
}
public void execute(QueueableContext context) {
// Calculate metrics
for (Account acc : accounts) {
acc.Total_Revenue__c = calculateRevenue(acc.Id);
acc.Customer_Tier__c = determineTier(acc.Total_Revenue__c);
}
update accounts;
// Chain next job
if (nextStep == 'notify') {
System.enqueueJob(new NotifyCustomersQueueable(accounts));
}
}
private Decimal calculateRevenue(Id accountId) {
// Complex revenue calculation
List<Opportunity> opps = [SELECT Amount FROM Opportunity WHERE AccountId = :accountId AND IsClosed = true];
Decimal total = 0;
for (Opportunity opp : opps) {
total += opp.Amount;
}
return total;
}
private String determineTier(Decimal revenue) {
if (revenue >= 1000000) return 'Platinum';
if (revenue >= 500000) return 'Gold';
if (revenue >= 100000) return 'Silver';
return 'Bronze';
}
}
// Called from trigger
trigger AccountTrigger on Account (after insert) {
System.enqueueJob(new CalculateAccountMetrics(Trigger.new, 'notify'));
}
String jobId = System.enqueueJob(new CalculateAccountMetrics(accounts, 'notify'));
// Query job status
AsyncApexJob jobStatus = [SELECT Id, Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];
System.debug('Job Status: ' + jobStatus.Status);
public class ChainedQueueable1 implements Queueable {
public void execute(QueueableContext context) {
// Do work
System.debug('Job 1 completed');
// Chain next job
if (CanExecuteNext()) {
System.enqueueJob(new ChainedQueueable2());
}
}
private Boolean CanExecuteNext() {
// Logic to decide if next job should run
return true;
}
}
Batch Apex is for processing large datasets. It breaks records into batches, processes each batch separately, and tracks progress.
global class UpdateAccountRatingBatch implements Database.Batchable<sObject>, Database.Stateful {
global Integer recordsProcessed = 0;
global Integer recordsWithError = 0;
// Start method - query records
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Name, Annual_Revenue__c, Rating
FROM Account
WHERE IsDeleted = false
]);
}
// Execute method - process batch
global void execute(Database.BatchableContext bc, List<Account> accounts) {
List<Account> accountsToUpdate = new List<Account>();
for (Account acc : accounts) {
if (acc.Annual_Revenue__c != null) {
if (acc.Annual_Revenue__c >= 1000000) {
acc.Rating = 'Hot';
} else if (acc.Annual_Revenue__c >= 100000) {
acc.Rating = 'Warm';
} else {
acc.Rating = 'Cold';
}
accountsToUpdate.add(acc);
recordsProcessed++;
}
}
// Use Database.update to continue on error
Database.SaveResult[] results = Database.update(accountsToUpdate, false);
for (Database.SaveResult result : results) {
if (!result.isSuccess()) {
recordsWithError++;
System.debug('Error: ' + result.getErrors());
}
}
}
// Finish method - handle completion
global void finish(Database.BatchableContext bc) {
System.debug('Batch job finished');
System.debug('Records processed: ' + recordsProcessed);
System.debug('Records with errors: ' + recordsWithError);
// Send email summary
sendCompletionEmail(bc.getJobId());
}
private void sendCompletionEmail(Id jobId) {
AsyncApexJob job = [SELECT Id, Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[]{UserInfo.getUserEmail()});
mail.setSubject('Batch Job Completed');
mail.setPlainTextBody('Batch processed ' + recordsProcessed + ' records with ' + recordsWithError + ' errors.');
Messaging.sendEmail(new Messaging.SingleEmailMessage[]{mail});
}
}
// Execute batch
Database.executeBatch(new UpdateAccountRatingBatch(), 200);
| Feature | @future | Queueable | Batch |
|---|---|---|---|
| Can accept objects | ❌ No | ✅ Yes | ✅ Yes |
| Can chain jobs | ❌ No | ✅ Yes | ❌ No |
| Best for large data | ❌ No | ❌ No | ✅ Yes |
| Job tracking | ❌ No | ✅ Yes | ✅ Yes |
Run a batch job on a schedule using Schedulable interface:
global class ScheduledBatchJob implements Schedulable {
global void execute(SchedulableContext ctx) {
Database.executeBatch(new UpdateAccountRatingBatch(), 200);
}
}
// Schedule it
String cronExpression = '0 0 0 * * ?'; // Daily at midnight
System.schedule('Update Account Ratings', cronExpression, new ScheduledBatchJob());