Asynchronous Apex – Future, Queueable, Batch with Use Cases

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.

1. @future Methods

@future is the simplest async approach. It executes in the future, separate from the request that scheduled it.

Use Case: Send Bulk Email Notifications


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');
}
  

Limitations of @future

2. Queueable Apex (Recommended)

Queueable Apex is more powerful than @future. It allows passing objects, returns a Job ID for tracking, and supports chaining.

Use Case: Chain Multiple Data Transformations


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'));
}
  

Monitoring Queueable Jobs


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);
  

Chaining Queueable Jobs


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;
    }
}
  

3. Batch Apex

Batch Apex is for processing large datasets. It breaks records into batches, processes each batch separately, and tracks progress.

Use Case: Update Millions of Records


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);
  

Comparison Table

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

Scheduled Batch Jobs

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());
  

Key Takeaways