Asynchronous Apex - Mastering Salesforce Development
Dive into the world of Asynchronous Apex, a critical skill for any Salesforce developer. This module will teach you how to overcome governor limits, handle long-running operations, and integrate with external systems efficiently.
1. Introduction to Asynchronous Apex
Salesforce operates within a multi-tenant environment, meaning resources are shared among many customers. To ensure fair usage and prevent any single organization from monopolizing resources, Salesforce imposes strict governor limits on synchronous (real-time) Apex transactions.
What is Asynchronous Apex?
Asynchronous Apex refers to Apex code that executes in the background, at a later time, rather than immediately. This execution occurs in a separate thread, allowing the main transaction to complete without waiting for the asynchronous process. This is crucial for:
- Bypassing Governor Limits: Asynchronous operations have higher or different governor limits (e.g., higher SOQL query limits, longer CPU time).
- Long-Running Operations: Performing complex calculations, large data processing, or extensive callouts that would exceed synchronous limits.
- Callouts to External Systems: HTTP callouts to external web services must be made asynchronously to prevent holding up the transaction.
- Better User Experience: Prevents users from waiting for long processes to complete, improving responsiveness of the UI.
When to use Asynchronous Apex?
Consider asynchronous Apex when your business logic involves:
- Making callouts to external web services.
- Processing a large number of records (e.g., batch updates).
- Performing complex calculations that require more CPU time.
- Executing operations that don't need to happen immediately.
- Chaining multiple operations together.
Salesforce provides several asynchronous Apex options, each suited for different use cases:
@future
Methods- Queueable Apex
- Batch Apex
- Scheduled Apex
2. @future Methods
@future
methods are a simple way to run Apex code in its own thread, at a later time. They are particularly useful for making callouts to external web services and for offloading long-running operations from the main transaction.
Key Characteristics:
- Static Method: Must be a
static
method. @future
Annotation: Must be annotated with@future
.- Void Return Type: Must return
void
. - Primitive Parameters: Parameters must be primitive data types (
String
,Integer
,Id
,List<String>
,Set<Id>
, etc.). You cannot pass sObjects or collections of sObjects directly. If you need to pass sObjects, pass their IDs and query them within the@future
method. - Callouts: If the method makes a callout, it must be annotated with
@future(callout=true)
. - No Chaining: You cannot chain
@future
methods directly (i.e., one@future
method cannot call another@future
method). - Limited Queue: Up to 50
@future
calls per transaction.
Example: Making a Callout
Apex Code Example:
public class ExternalServiceCallout {
@future(callout=true)
public static void sendAccountToExternalSystem(Set<Id> accountIds) {
List<Account> accounts = [SELECT Id, Name, AnnualRevenue FROM Account WHERE Id IN :accountIds];
// Simulate a callout to an external system
System.debug('Sending ' + accounts.size() + ' accounts to external system asynchronously.');
for (Account acc : accounts) {
System.debug('Processing Account: ' + acc.Name + ' (' + acc.Id + ')');
// Example: Http request setup
// HttpRequest req = new HttpRequest();
// req.setEndpoint('http://api.external.com/accounts');
// req.setMethod('POST');
// req.setBody(JSON.serialize(acc));
// Http http = new Http();
// HTTPResponse res = http.send(req);
// System.debug('Callout response: ' + res.getBody());
}
}
}
// How to call it from a trigger or other Apex code:
trigger AccountAfterInsert on Account (after insert) {
Set<Id> newAccountIds = new Set<Id>();
for (Account acc : Trigger.new) {
newAccountIds.add(acc.Id);
}
if (!newAccountIds.isEmpty()) {
ExternalServiceCallout.sendAccountToExternalSystem(newAccountIds);
}
}
Limitations:
- Cannot monitor progress directly.
- No guaranteed execution order for multiple
@future
calls. - Cannot pass sObjects directly, requiring extra SOQL queries.
3. Queueable Apex
Queueable Apex provides a more flexible and robust way to run asynchronous Apex code compared to @future
methods. It implements the Queueable
interface and allows for chaining jobs and passing sObjects.
Key Characteristics:
Queueable
Interface: Class must implementDatabase.Queueable
.execute
Method: Must contain a singleexecute
method.- sObject Parameters: Can accept sObjects and collections of sObjects as parameters.
- Chaining: A Queueable job can enqueue another Queueable job (up to 50 jobs in a chain). This allows for sequential processing of complex logic.
- Job ID: The
System.enqueueJob
method returns a job ID, which can be used to monitor the job's progress. - Higher Limits: Generally has higher limits than
@future
methods, especially for heap size.
Example: Chaining Updates
Apex Code Example:
public class AccountContactUpdater implements Queueable {
private List<Account> accountsToProcess;
private Boolean updateContacts;
public AccountContactUpdater(List<Account> accounts, Boolean updateContactsFlag) {
this.accountsToProcess = accounts;
this.updateContacts = updateContactsFlag;
}
public void execute(QueueableContext context) {
System.debug('Executing AccountContactUpdater Queueable job. Batch ID: ' + context.getJobId());
List<Contact> contactsToUpdate = new List<Contact>();
for (Account acc : accountsToProcess) {
// Logic to update account fields (e.g., based on some criteria)
acc.Description = 'Processed by Queueable Apex.';
// update acc; // DML on triggering records should be avoided if possible, or handled carefully
if (updateContacts) {
// Query related contacts and update them
for (Contact con : [SELECT Id, FirstName, LastName, MailingCity FROM Contact WHERE AccountId = :acc.Id]) {
con.MailingCity = acc.BillingCity; // Update contact city based on account
contactsToUpdate.add(con);
}
}
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
System.debug('Updated ' + contactsToUpdate.size() + ' contacts.');
}
// Example of chaining another Queueable job
// If there's more work to do, enqueue another job
if (accountsToProcess.size() > 100) { // Arbitrary condition for chaining
System.debug('Chaining another AccountContactUpdater job.');
System.enqueueJob(new AccountContactUpdater(accountsToProcess.subList(100, accountsToProcess.size()), updateContacts));
}
}
}
// How to call it from a trigger or other Apex code:
trigger AccountQueueableTrigger on Account (after insert, after update) {
if (Trigger.isAfter) {
List<Account> accountsToProcess = new List<Account>();
for (Account acc : Trigger.new) {
// Add accounts that meet certain criteria for processing
if (acc.AnnualRevenue != null && acc.AnnualRevenue > 1000000) {
accountsToProcess.add(acc);
}
}
if (!accountsToProcess.isEmpty()) {
System.enqueueJob(new AccountContactUpdater(accountsToProcess, true));
}
}
}
4. Batch Apex
Batch Apex is designed for processing large numbers of records (up to 50 million) that would exceed normal governor limits. It breaks down the processing into smaller, manageable chunks (batches) that can be processed asynchronously.
Key Characteristics:
Database.Batchable
Interface: Class must implementDatabase.Batchable<sObject>
(or a custom type).- Three Methods:
start(Database.BatchableContext bc)
: Called once at the beginning. Used to collect the records or objects to be passed to theexecute
method. Typically returns aDatabase.QueryLocator
for SOQL queries or anIterable
for custom logic.execute(Database.BatchableContext bc, List<sObject> scope)
: Called for each batch of records. Thescope
parameter contains the chunk of records to process (up to 200 by default). This is where your main processing logic resides.finish(Database.BatchableContext bc)
: Called once after all batches are processed. Used for post-processing operations like sending email notifications or logging.
- Higher Limits: Each
execute
method call runs in its own set of governor limits. - Monitoring: Batch jobs can be monitored in the Apex Jobs page in Setup.
- Queue: Up to 5 concurrent batch jobs can run at a time.
Example: Mass Data Update
Apex Code Example:
public class AccountRatingBatch implements Database.Batchable<sObject>, Database.AllowsCallouts {
public String query; // SOQL query to select accounts
public String newRating; // New rating to set
public AccountRatingBatch(String q, String rating) {
this.query = q;
this.newRating = rating;
}
public Database.QueryLocator start(Database.BatchableContext bc) {
System.debug('Batch Start: ' + query);
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
System.debug('Batch Execute: Processing ' + scope.size() + ' accounts.');
List<Account> accountsToUpdate = new List<Account>();
for (Account acc : scope) {
acc.Rating = newRating;
accountsToUpdate.add(acc);
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate;
}
// Example: Make a callout for each batch (if Database.AllowsCallouts is implemented)
// ExternalServiceCallout.sendBatchSummary(scope.size());
}
public void finish(Database.BatchableContext bc) {
AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems FROM AsyncApexJob WHERE Id = :bc.getJobId()];
System.debug('Batch Finish: Job ' + job.Id + ' finished with status ' + job.Status);
System.debug('Processed ' + job.JobItemsProcessed + ' items with ' + job.NumberOfErrors + ' errors.');
// Send an email notification upon completion
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[] {'admin@example.com'};
mail.setToAddresses(toAddresses);
mail.setSubject('Account Rating Batch Job Completed');
mail.setPlainTextBody('The Account Rating batch job has completed. Status: ' + job.Status + '. Processed: ' + job.JobItemsProcessed + '. Errors: ' + job.NumberOfErrors);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
// How to execute the batch job:
// From Developer Console or anonymous Apex:
Database.executeBatch(new AccountRatingBatch('SELECT Id, Name, Rating FROM Account WHERE CreatedDate = LAST_N_DAYS:30', 'Hot'), 200);
// The second parameter (200) is the batch size.
5. Scheduled Apex
Scheduled Apex allows you to execute Apex classes at specific times using the Salesforce scheduler. This is ideal for tasks that need to run regularly, such as daily data cleanups, weekly report generation, or monthly data synchronizations.
Key Characteristics:
Schedulable
Interface: Class must implementSchedulable
.execute
Method: Must contain a singleexecute
method that takes aSchedulableContext
parameter.- Cron Expression: Scheduling is done using a Cron expression (e.g., '0 0 1 * * ?' for 1 AM daily) or by specifying a frequency in the UI.
- Single Instance: Only one instance of a specific Scheduled Apex class can be scheduled at a time.
- Combine with Batch Apex: Often used to schedule Batch Apex jobs for large data processing.
Example: Daily Data Cleanup
Apex Code Example:
public class DailyDataCleanupScheduler implements Schedulable {
public void execute(SchedulableContext sc) {
System.debug('Daily Data Cleanup: Starting scheduled job.');
// Example: Delete old records
List<Task> oldTasks = [SELECT Id FROM Task WHERE CreatedDate < LAST_N_DAYS:365 LIMIT 10000];
if (!oldTasks.isEmpty()) {
delete oldTasks;
System.debug('Deleted ' + oldTasks.size() + ' old tasks.');
}
// Or, more commonly, kick off a Batch Apex job for large-scale cleanup
Database.executeBatch(new OldCaseCleanupBatch()); // Assuming OldCaseCleanupBatch is a Batch Apex class
}
}
// How to schedule it:
// 1. From Developer Console (anonymous Apex):
System.schedule('Daily Cleanup Job', '0 0 0 * * ?', new DailyDataCleanupScheduler()); // Runs every day at midnight (GMT)
// 2. From Salesforce UI: Setup -> Apex Classes -> Schedule Apex
// You can specify the class, job name, and frequency there.
6. Choosing the Right Asynchronous Tool
Selecting the appropriate asynchronous Apex tool depends on your specific requirements:
@future
Methods:- Use when: You need to perform a single callout or offload a small, self-contained operation immediately after a transaction, and you don't need to monitor its progress or chain it.
- Limitations: Cannot pass sObjects, no chaining, limited monitoring.
- Queueable Apex:
- Use when: You need to pass sObjects, chain jobs together, or require more flexibility and higher limits than
@future
methods for a single, non-batch operation. - Advantages: sObject parameters, chaining, job ID for monitoring.
- Use when: You need to pass sObjects, chain jobs together, or require more flexibility and higher limits than
- Batch Apex:
- Use when: You need to process a very large number of records (thousands to millions) that would hit governor limits in a single transaction. Ideal for mass data updates, deletions, or calculations.
- Advantages: Processes records in chunks, separate governor limits per chunk, robust error handling, built-in progress monitoring.
- Scheduled Apex:
- Use when: You need to run a specific Apex class (often a Batch Apex job or a Queueable job) at a predetermined time or on a recurring schedule.
- Advantages: Automation of recurring tasks, time-based execution.
General Rule of Thumb:
- Start with declarative automation (Flows, Process Builder).
- If code is required, consider
@future
for simple fire-and-forget callouts or small background tasks. - If you need to pass sObjects or chain jobs, move to Queueable Apex.
- For large data volumes, use Batch Apex.
- For recurring tasks, use Scheduled Apex (often to kick off a Batch or Queueable job).
7. Monitoring & Debugging Asynchronous Apex
Monitoring and debugging asynchronous Apex jobs are crucial for ensuring your background processes are running correctly and efficiently.
Monitoring Asynchronous Jobs:
- Setup -> Apex Jobs: This page provides a comprehensive list of all asynchronous Apex jobs (Batch, Queueable, Scheduled, and
@future
methods). You can see their status (Queued, Processing, Completed, Failed), submission date, completion date, and any errors. - Setup -> Scheduled Jobs: Specifically for Scheduled Apex, this page shows all jobs currently scheduled to run. You can delete or reschedule jobs from here.
- AsyncApexJob Object: You can query the
AsyncApexJob
sObject to programmatically get information about your jobs. This is useful for building custom monitoring tools or for checking job status within Apex code.
Apex Code Example:
// Example SOQL query to get job status
List<AsyncApexJob> jobs = [SELECT Id, JobType, Status, ApexClass.Name, JobItemsProcessed, TotalJobItems, NumberOfErrors
FROM AsyncApexJob
WHERE JobType IN ('BatchApex', 'Queueable', 'Future')
ORDER BY CreatedDate DESC LIMIT 10];
for (AsyncApexJob job : jobs) {
System.debug('Job ID: ' + job.Id + ', Type: ' + job.JobType + ', Status: ' + job.Status + ', Class: ' + job.ApexClass.Name);
if (job.NumberOfErrors > 0) {
System.debug('Errors: ' + job.NumberOfErrors);
}
}
Debugging Asynchronous Jobs:
- Debug Logs: The primary tool for debugging. Set up debug logs for the user who initiated the asynchronous job (or the Automated Process user for scheduled jobs). Ensure logging levels are appropriate (e.g., Apex Code: FINEST, Workflow: FINEST).
- System.debug(): Use
System.debug()
statements liberally within your asynchronous code to track execution flow, variable values, and DML operations. - Exception Handling: Implement robust
try-catch
blocks within your asynchronous methods. Log any exceptions to a custom error object or usingSystem.debug()
. For Batch Apex, exceptions in theexecute
method will be reported in theAsyncApexJob
record. - Limits.get*() Methods: Use methods from the
Limits
class (e.g.,Limits.getLimitQueries()
,Limits.getQueries()
) to monitor governor limit consumption within yourexecute
methods, especially in Batch Apex.
Apex Code Example:
public class DebuggingBatchExample implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name FROM Account');
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
System.debug('--- Batch Execute Start ---');
System.debug('Current Heap Size: ' + Limits.getHeapSize() + ' / ' + Limits.getLimitHeapSize());
System.debug('Queries Used: ' + Limits.getQueries() + ' / ' + Limits.getLimitQueries());
try {
for (Account acc : scope) {
// Simulate an error for debugging
if (acc.Name == 'Error Account') {
throw new MyCustomException('Account named "Error Account" encountered!');
}
acc.Description = 'Processed by Debugging Batch';
}
update scope;
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'Error in batch execute: ' + e.getMessage() + ' at line ' + e.getLineNumber());
// Optionally, log to a custom error object
// Error_Log__c error = new Error_Log__c(Message__c = e.getMessage(), Stack_Trace__c = e.getStackTraceString());
// insert error;
}
System.debug('--- Batch Execute End ---');
}
public void finish(Database.BatchableContext bc) {
// Post-processing and final logging
System.debug('Batch Finished.');
}
public class MyCustomException extends Exception {}
}
8. Conclusion & Best Practices
Asynchronous Apex is an indispensable part of Salesforce development, enabling you to build robust, scalable, and efficient applications that can handle complex business requirements and large data volumes without hitting governor limits.
Key Takeaways:
- Governor Limits: Asynchronous Apex helps you work around the stricter limits of synchronous transactions.
- Callouts: HTTP callouts must be made asynchronously.
- Tool Selection: Choose the right tool (
@future
, Queueable, Batch, Scheduled) based on the specific use case (simple callout, chaining, large data volume, recurring tasks). - Bulkification: Always write asynchronous code with bulkification in mind, avoiding SOQL/DML inside loops.
- Error Handling: Implement comprehensive
try-catch
blocks and logging for better debugging and monitoring. - Monitoring: Regularly check Apex Jobs and Scheduled Jobs in Setup, or query
AsyncApexJob
.
General Best Practices for Asynchronous Apex:
- Keep it Lean: Asynchronous methods should focus on their core task. Delegate complex logic to helper classes.
- Idempotency: Design your asynchronous operations to be idempotent, meaning running them multiple times with the same inputs produces the same result without unintended side effects. This helps in case of retries or unexpected re-executions.
- Test Thoroughly: Write comprehensive unit tests for all asynchronous Apex, including positive, negative, and bulk scenarios. Ensure proper governor limit testing.
- Consider Platform Events: For complex, decoupled integrations or event-driven architectures, consider Platform Events as another powerful asynchronous option.
- Use the Right Tool for the Job: Don't just pick the first asynchronous method you know. Evaluate the requirements carefully against the strengths and weaknesses of each option.
By mastering these asynchronous patterns, you'll be able to build more powerful and resilient Salesforce applications.