Mastering Queueable Apex in Salesforce

Asynchronous processing is crucial for building scalable Salesforce applications that can handle large data volumes without hitting governor limits.

Among the various asynchronous processing options in Salesforce, Queueable Apex stands out as one of the most flexible and powerful tools available to developers.

What is Queueable Apex?

Queueable Apex is an asynchronous processing framework that allows you to execute long-running operations in the background. It combines the benefits of both @future methods and Batch Apex, providing:

  • Higher governor limits compared to synchronous execution
  • Monitoring capabilities through the Apex Jobs queue
  • Support for complex data types as parameters
  • Chaining capabilities for sequential processing
  • Better error handling and recovery mechanisms

Key Benefits of Queueable Apex

1. Flexible Parameter Handling

Unlike @future methods that only accept primitive data types, Queueable classes can work with:

  • Custom objects (sObjects)
  • Collections (Lists, Sets, Maps)
  • Custom Apex classes
  • Any serializable data structure

2. Monitoring and Management

  • Track job status in Setup → Apex Jobs
  • Get job IDs for monitoring
  • Built-in retry mechanisms
  • Detailed error logging

3. Chaining Support

Execute multiple queueable jobs in sequence, perfect for complex multi-step processes.

Basic Queueable Implementation

Let’s start with a simple example that demonstrates the core concepts:

public class AccountUpdateQueueable implements Queueable {
    private List<Account> accountsToUpdate;
    private String updateReason;
    
    public AccountUpdateQueueable(List<Account> accounts, String reason) {
        this.accountsToUpdate = accounts;
        this.updateReason = reason;
    }
    
    public void execute(QueueableContext context) {
        try {
            // Process accounts in batches to avoid governor limits
            List<Account> processedAccounts = new List<Account>();
            
            for (Account acc : accountsToUpdate) {
                acc.Description = 'Updated: ' + updateReason + ' on ' + DateTime.now();
                acc.LastModifiedDate = DateTime.now();
                processedAccounts.add(acc);
            }
            
            // Perform DML operation
            if (!processedAccounts.isEmpty()) {
                update processedAccounts;
                System.debug('Successfully updated ' + processedAccounts.size() + ' accounts');
            }
            
        } catch (Exception e) {
            System.debug('Error in AccountUpdateQueueable: ' + e.getMessage());
            // Log error or send notification
            handleError(e, context.getJobId());
        }
    }
    
    private void handleError(Exception e, Id jobId) {
        // Create error log record or send email notification
        System.debug('Job ID: ' + jobId + ', Error: ' + e.getMessage());
    }
}

Enqueuing the Job

// Create and enqueue the job
List<Account> accounts = [SELECT Id, Name, Description FROM Account LIMIT 100];
AccountUpdateQueueable job = new AccountUpdateQueueable(accounts, 'Bulk Update Process');

// Enqueue and get job ID for monitoring
Id jobId = System.enqueueJob(job);
System.debug('Job enqueued with ID: ' + jobId);

Advanced Example: Data Migration Queueable

Here’s a more complex example that demonstrates processing large datasets:

public class DataMigrationQueueable implements Queueable {
    private static final Integer BATCH_SIZE = 200;
    private List<Legacy_Record__c> recordsToMigrate;
    private Integer currentBatch;
    private Integer totalBatches;
    
    public DataMigrationQueueable(List<Legacy_Record__c> records) {
        this.recordsToMigrate = records;
        this.currentBatch = 1;
        this.totalBatches = (Integer) Math.ceil((Decimal) records.size() / BATCH_SIZE);
    }
    
    public DataMigrationQueueable(List<Legacy_Record__c> records, Integer batch, Integer total) {
        this.recordsToMigrate = records;
        this.currentBatch = batch;
        this.totalBatches = total;
    }
    
    public void execute(QueueableContext context) {
        List<Modern_Record__c> newRecords = new List<Modern_Record__c>();
        List<Legacy_Record__c> recordsToUpdate = new List<Legacy_Record__c>();
        
        try {
            // Process current batch
            for (Legacy_Record__c legacy : recordsToMigrate) {
                // Transform legacy record to new format
                Modern_Record__c newRecord = transformRecord(legacy);
                newRecords.add(newRecord);
                
                // Mark legacy record as migrated
                legacy.Migration_Status__c = 'Migrated';
                legacy.Migration_Date__c = DateTime.now();
                recordsToUpdate.add(legacy);
            }
            
            // Insert new records
            if (!newRecords.isEmpty()) {
                insert newRecords;
            }
            
            // Update legacy records
            if (!recordsToUpdate.isEmpty()) {
                update recordsToUpdate;
            }
            
            System.debug('Batch ' + currentBatch + ' of ' + totalBatches + ' completed successfully');
            
            // Chain next batch if there are more records to process
            chainNextBatch();
            
        } catch (Exception e) {
            System.debug('Error in DataMigrationQueueable: ' + e.getMessage());
            handleMigrationError(e, context.getJobId());
        }
    }
    
    private Modern_Record__c transformRecord(Legacy_Record__c legacy) {
        return new Modern_Record__c(
            Name = legacy.Name,
            Legacy_ID__c = legacy.Id,
            Transformed_Data__c = legacy.Old_Data__c,
            Migration_Source__c = 'Queueable Migration'
        );
    }
    
    private void chainNextBatch() {
        if (currentBatch < totalBatches) {
            // Query next batch of records
            List<Legacy_Record__c> nextBatch = [
                SELECT Id, Name, Old_Data__c 
                FROM Legacy_Record__c 
                WHERE Migration_Status__c != 'Migrated' 
                LIMIT :BATCH_SIZE
            ];
            
            if (!nextBatch.isEmpty()) {
                DataMigrationQueueable nextJob = new DataMigrationQueueable(
                    nextBatch, 
                    currentBatch + 1, 
                    totalBatches
                );
                System.enqueueJob(nextJob);
            }
        }
    }
    
    private void handleMigrationError(Exception e, Id jobId) {
        // Send email notification to administrators
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
        email.setToAddresses(new String[]{'[email protected]'});
        email.setSubject('Data Migration Error - Job ID: ' + jobId);
        email.setPlainTextBody('Error occurred during data migration: ' + e.getMessage());
        
        Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
    }
}

Queueable Chaining: Sequential Processing

Queueable chaining allows you to execute multiple jobs in sequence, which is perfect for complex workflows that require ordered processing.

Basic Chaining Example

public class ChainedProcessingQueueable implements Queueable {
    private String processStep;
    private Map<String, Object> processData;
    
    public ChainedProcessingQueueable(String step, Map<String, Object> data) {
        this.processStep = step;
        this.processData = data;
    }
    
    public void execute(QueueableContext context) {
        try {
            switch on processStep {
                when 'STEP_1' {
                    executeStep1();
                    chainNextStep('STEP_2');
                }
                when 'STEP_2' {
                    executeStep2();
                    chainNextStep('STEP_3');
                }
                when 'STEP_3' {
                    executeStep3();
                    // Final step - no chaining needed
                    System.debug('All processing steps completed successfully');
                }
            }
        } catch (Exception e) {
            System.debug('Error in step ' + processStep + ': ' + e.getMessage());
            handleChainError(e, context.getJobId());
        }
    }
    
    private void executeStep1() {
        // Step 1: Data validation and preparation
        System.debug('Executing Step 1: Data Validation');
        List<Account> accounts = (List<Account>) processData.get('accounts');
        
        for (Account acc : accounts) {
            if (String.isBlank(acc.Name)) {
                acc.Name = 'Default Account Name';
            }
        }
        
        update accounts;
        processData.put('step1_completed', true);
    }
    
    private void executeStep2() {
        // Step 2: Related record processing
        System.debug('Executing Step 2: Related Record Processing');
        List<Account> accounts = (List<Account>) processData.get('accounts');
        List<Contact> contactsToCreate = new List<Contact>();
        
        for (Account acc : accounts) {
            Contact newContact = new Contact(
                LastName = acc.Name + ' Contact',
                AccountId = acc.Id,
                Email = 'contact@' + acc.Name.toLowerCase().replaceAll(' ', '') + '.com'
            );
            contactsToCreate.add(newContact);
        }
        
        insert contactsToCreate;
        processData.put('contacts_created', contactsToCreate.size());
    }
    
    private void executeStep3() {
        // Step 3: Final cleanup and notifications
        System.debug('Executing Step 3: Final Processing');
        Integer contactsCreated = (Integer) processData.get('contacts_created');
        
        // Send completion notification
        sendCompletionNotification(contactsCreated);
    }
    
    private void chainNextStep(String nextStep) {
        ChainedProcessingQueueable nextJob = new ChainedProcessingQueueable(nextStep, processData);
        System.enqueueJob(nextJob);
    }
    
    private void handleChainError(Exception e, Id jobId) {
        System.debug('Chain broken at step: ' + processStep + ', Job ID: ' + jobId);
        // Implement error recovery or notification logic
    }
    
    private void sendCompletionNotification(Integer recordCount) {
        System.debug('Processing completed. ' + recordCount + ' contacts created.');
    }
}

Advanced Chaining with State Management

public class StatefulChainQueueable implements Queueable {
    
    public class ProcessState {
        public Integer currentStep = 1;
        public Integer totalSteps = 5;
        public Map<String, Object> data = new Map<String, Object>();
        public List<String> errors = new List<String>();
        public DateTime startTime = DateTime.now();
    }
    
    private ProcessState state;
    
    public StatefulChainQueueable(ProcessState processState) {
        this.state = processState;
    }
    
    public void execute(QueueableContext context) {
        try {
            System.debug('Executing step ' + state.currentStep + ' of ' + state.totalSteps);
            
            switch on state.currentStep {
                when 1 { processDataExtraction(); }
                when 2 { processDataTransformation(); }
                when 3 { processDataValidation(); }
                when 4 { processDataLoad(); }
                when 5 { processCleanupAndNotification(); }
            }
            
            // Continue to next step if not the last one
            if (state.currentStep < state.totalSteps) {
                state.currentStep++;
                StatefulChainQueueable nextJob = new StatefulChainQueueable(state);
                System.enqueueJob(nextJob);
            } else {
                System.debug('All steps completed in: ' + 
                    (DateTime.now().getTime() - state.startTime.getTime()) + 'ms');
            }
            
        } catch (Exception e) {
            state.errors.add('Step ' + state.currentStep + ': ' + e.getMessage());
            handleStepError(e, context.getJobId());
        }
    }
    
    private void processDataExtraction() {
        // Extract data from external source or complex query
        List<sObject> extractedData = Database.query(
            'SELECT Id, Name FROM Account WHERE CreatedDate = TODAY LIMIT 1000'
        );
        state.data.put('extracted_records', extractedData);
    }
    
    private void processDataTransformation() {
        List<Account> accounts = (List<Account>) state.data.get('extracted_records');
        for (Account acc : accounts) {
            acc.Description = 'Processed on: ' + DateTime.now();
        }
        state.data.put('transformed_records', accounts);
    }
    
    private void processDataValidation() {
        List<Account> accounts = (List<Account>) state.data.get('transformed_records');
        List<Account> validAccounts = new List<Account>();
        
        for (Account acc : accounts) {
            if (acc.Name != null && acc.Name.length() > 0) {
                validAccounts.add(acc);
            }
        }
        
        state.data.put('valid_records', validAccounts);
        state.data.put('validation_count', validAccounts.size());
    }
    
    private void processDataLoad() {
        List<Account> accounts = (List<Account>) state.data.get('valid_records');
        update accounts;
        state.data.put('updated_count', accounts.size());
    }
    
    private void processCleanupAndNotification() {
        Integer updatedCount = (Integer) state.data.get('updated_count');
        System.debug('Processing completed. Updated ' + updatedCount + ' records.');
        
        // Send notification or create log record
        createProcessingLog();
    }
    
    private void createProcessingLog() {
        // Create a custom log record to track the processing
        Processing_Log__c log = new Processing_Log__c(
            Process_Name__c = 'StatefulChainQueueable',
            Start_Time__c = state.startTime,
            End_Time__c = DateTime.now(),
            Records_Processed__c = (Integer) state.data.get('updated_count'),
            Status__c = state.errors.isEmpty() ? 'Success' : 'Completed with Errors',
            Error_Details__c = String.join(state.errors, '\n')
        );
        insert log;
    }
    
    private void handleStepError(Exception e, Id jobId) {
        System.debug('Error in step ' + state.currentStep + ': ' + e.getMessage());
        // Decide whether to continue or stop the chain based on error severity
    }
}

Best Practices and Guidelines

1. Governor Limits Awareness

public class GovernorLimitAwareQueueable implements Queueable {
    private List<sObject> recordsToProcess;
    private static final Integer MAX_DML_OPERATIONS = 150; // Safe limit
    private static final Integer MAX_SOQL_QUERIES = 100;   // Safe limit
    
    public void execute(QueueableContext context) {
        // Monitor limits throughout execution
        Integer dmlCount = Limits.getDmlStatements();
        Integer queryCount = Limits.getQueries();
        
        if (dmlCount > MAX_DML_OPERATIONS || queryCount > MAX_SOQL_QUERIES) {
            // Chain to new job to reset limits
            chainContinuation();
            return;
        }
        
        // Continue processing...
    }
    
    private void chainContinuation() {
        // Create new job with remaining records
        System.enqueueJob(new GovernorLimitAwareQueueable(recordsToProcess));
    }
}

2. Error Handling Strategies

public class ResilientQueueable implements Queueable {
    private Integer retryCount = 0;
    private static final Integer MAX_RETRIES = 3;
    
    public void execute(QueueableContext context) {
        try {
            // Risky operation
            performRiskyOperation();
        } catch (Exception e) {
            handleRetryLogic(e);
        }
    }
    
    private void handleRetryLogic(Exception e) {
        if (retryCount < MAX_RETRIES) {
            retryCount++;
            System.debug('Retrying operation, attempt: ' + retryCount);
            
            // Exponential backoff could be implemented here
            ResilientQueueable retryJob = new ResilientQueueable();
            retryJob.retryCount = this.retryCount;
            
            System.enqueueJob(retryJob);
        } else {
            System.debug('Max retries exceeded, logging failure');
            logFinalFailure(e);
        }
    }
    
    private void performRiskyOperation() {
        // Implementation here
    }
    
    private void logFinalFailure(Exception e) {
        // Log to custom object or send notification
    }
}

3. Testing Queueable Classes

@IsTest
public class QueueableTest {
    
    @TestSetup
    static void setupTestData() {
        List<Account> testAccounts = new List<Account>();
        for (Integer i = 0; i < 100; i++) {
            testAccounts.add(new Account(Name = 'Test Account ' + i));
        }
        insert testAccounts;
    }
    
    @IsTest
    static void testAccountUpdateQueueable() {
        List<Account> accounts = [SELECT Id, Name FROM Account];
        
        Test.startTest();
        AccountUpdateQueueable job = new AccountUpdateQueueable(accounts, 'Test Update');
        Id jobId = System.enqueueJob(job);
        Test.stopTest();
        
        // Verify results
        List<Account> updatedAccounts = [SELECT Id, Description FROM Account];
        for (Account acc : updatedAccounts) {
            System.assert(acc.Description.contains('Test Update'), 'Account not updated correctly');
        }
        
        // Verify job completed
        AsyncApexJob jobRecord = [SELECT Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];
        System.assertEquals('Completed', jobRecord.Status, 'Job should complete successfully');
        System.assertEquals(0, jobRecord.NumberOfErrors, 'Job should have no errors');
    }
    
    @IsTest
    static void testQueueableChaining() {
        List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 10];
        Map<String, Object> processData = new Map<String, Object>{'accounts' => accounts};
        
        Test.startTest();
        ChainedProcessingQueueable job = new ChainedProcessingQueueable('STEP_1', processData);
        System.enqueueJob(job);
        Test.stopTest();
        
        // Verify all steps executed (in test context, chained jobs execute synchronously)
        List<Contact> createdContacts = [SELECT Id FROM Contact WHERE AccountId IN :accounts];
        System.assert(!createdContacts.isEmpty(), 'Contacts should be created by chained process');
    }
    
    @IsTest
    static void testFinalizerExecution() {
        List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 5];
        
        Test.startTest();
        RobustProcessingQueueable job = new RobustProcessingQueueable(accounts, 'TEST_PROCESS');
        System.enqueueJob(job);
        Test.stopTest();
        
        // Verify finalizer executed (success case)
        List<Success_Log__c> successLogs = [SELECT Id, Process_ID__c FROM Success_Log__c WHERE Process_ID__c = 'TEST_PROCESS'];
        System.assert(!successLogs.isEmpty(), 'Success log should be created by finalizer');
    }
    
    @IsTest
    static void testFinalizerWithError() {
        List<Account> accounts = new List<Account>();
        // Create more than 500 accounts to trigger error condition
        for (Integer i = 0; i < 501; i++) {
            accounts.add(new Account(Name = 'Error Test Account ' + i));
        }
        insert accounts;
        
        Test.startTest();
        RobustProcessingQueueable job = new RobustProcessingQueueable(accounts, 'ERROR_TEST');
        System.enqueueJob(job);
        Test.stopTest();
        
        // Verify error handling in finalizer
        List<Error_Log__c> errorLogs = [SELECT Id, Process_ID__c FROM Error_Log__c WHERE Process_ID__c = 'ERROR_TEST'];
        System.assert(!errorLogs.isEmpty(), 'Error log should be created by finalizer');
    }
}

Performance Optimization Tips

1. Batch Processing Strategy

public class OptimizedBatchQueueable implements Queueable {
    private List<sObject> recordsToProcess;
    private Integer batchSize;
    private Integer currentIndex = 0;
    
    public OptimizedBatchQueueable(List<sObject> records, Integer batchSize) {
        this.recordsToProcess = records;
        this.batchSize = batchSize;
    }
    
    public void execute(QueueableContext context) {
        List<sObject> currentBatch = new List<sObject>();
        Integer endIndex = Math.min(currentIndex + batchSize, recordsToProcess.size());
        
        // Process current batch
        for (Integer i = currentIndex; i < endIndex; i++) {
            currentBatch.add(recordsToProcess[i]);
        }
        
        // Perform DML on current batch
        if (!currentBatch.isEmpty()) {
            processBatch(currentBatch);
        }
        
        // Chain next batch if more records exist
        if (endIndex < recordsToProcess.size()) {
            OptimizedBatchQueueable nextBatch = new OptimizedBatchQueueable(recordsToProcess, batchSize);
            nextBatch.currentIndex = endIndex;
            System.enqueueJob(nextBatch);
        }
    }
    
    private void processBatch(List<sObject> batch) {
        // Optimized processing logic
        update batch;
    }
}

2. Memory Management

public class MemoryOptimizedQueueable implements Queueable {
    private String queryString;
    private Integer offset = 0;
    private static final Integer BATCH_SIZE = 1000;
    
    public MemoryOptimizedQueueable(String query, Integer startOffset) {
        this.queryString = query;
        this.offset = startOffset;
    }
    
    public void execute(QueueableContext context) {
        // Query only what we need for this batch
        String batchQuery = queryString + ' LIMIT ' + BATCH_SIZE + ' OFFSET ' + offset;
        List<sObject> records = Database.query(batchQuery);
        
        if (!records.isEmpty()) {
            // Process records
            processRecords(records);
            
            // Clear the list to free memory
            records.clear();
            
            // Chain next batch
            MemoryOptimizedQueueable nextJob = new MemoryOptimizedQueueable(queryString, offset + BATCH_SIZE);
            System.enqueueJob(nextJob);
        }
    }
    
    private void processRecords(List<sObject> records) {
        // Process each record individually to minimize memory usage
        for (sObject record : records) {
            processIndividualRecord(record);
        }
    }
    
    private void processIndividualRecord(sObject record) {
        // Individual processing logic
    }
}

Monitoring and Debugging

1. Custom Monitoring Solution

public class MonitoredQueueable implements Queueable, Finalizer {
    private String processName;
    private DateTime startTime;
    private Map<String, Object> metrics;
    
    public MonitoredQueueable(String name) {
        this.processName = name;
        this.startTime = DateTime.now();
        this.metrics = new Map<String, Object>();
    }
    
    public void execute(QueueableContext context) {
        // Record job start
        recordJobStart(context.getJobId());
        
        try {
            // Your processing logic here
            performProcessing();
            
            // Record success metrics
            metrics.put('status', 'SUCCESS');
            metrics.put('records_processed', 100); // Example
            
        } catch (Exception e) {
            metrics.put('status', 'ERROR');
            metrics.put('error_message', e.getMessage());
            throw e;
        }
    }
    
    public void execute(FinalizerContext context) {
        // Record job completion
        recordJobCompletion(context.getAsyncApexJobId(), context.getException());
    }
    
    private void recordJobStart(Id jobId) {
        Job_Monitoring__c monitor = new Job_Monitoring__c(
            Job_ID__c = String.valueOf(jobId),
            Process_Name__c = processName,
            Start_Time__c = startTime,
            Status__c = 'RUNNING'
        );
        insert monitor;
    }
    
    private void recordJobCompletion(Id jobId, Exception error) {
        Job_Monitoring__c monitor = [SELECT Id FROM Job_Monitoring__c WHERE Job_ID__c = :String.valueOf(jobId)];
        monitor.End_Time__c = DateTime.now();
        monitor.Duration_Ms__c = DateTime.now().getTime() - startTime.getTime();
        monitor.Status__c = error == null ? 'COMPLETED' : 'FAILED';
        monitor.Error_Details__c = error?.getMessage();
        monitor.Metrics__c = JSON.serialize(metrics);
        
        update monitor;
    }
    
    private void performProcessing() {
        // Your processing logic
    }
}

2. Debug Logging Utility

public class QueueableLogger {
    private static List<String> logEntries = new List<String>();
    private static String currentJobId;
    
    public static void setJobId(Id jobId) {
        currentJobId = String.valueOf(jobId);
    }
    
    public static void log(String level, String message) {
        String timestamp = DateTime.now().format('yyyy-MM-dd HH:mm:ss.SSS');
        String logEntry = timestamp + ' [' + level + '] Job: ' + currentJobId + ' - ' + message;
        
        logEntries.add(logEntry);
        System.debug(logEntry);
        
        // Flush logs periodically to avoid memory issues
        if (logEntries.size() >= 100) {
            flushLogs();
        }
    }
    
    public static void info(String message) {
        log('INFO', message);
    }
    
    public static void warn(String message) {
        log('WARN', message);
    }
    
    public static void error(String message) {
        log('ERROR', message);
    }
    
    public static void flushLogs() {
        if (!logEntries.isEmpty()) {
            // Save logs to custom object or external system
            Debug_Log__c debugLog = new Debug_Log__c(
                Job_ID__c = currentJobId,
                Log_Entries__c = String.join(logEntries, '\n'),
                Timestamp__c = DateTime.now()
            );
            
            insert debugLog;
            logEntries.clear();
        }
    }
}

// Usage in Queueable class:
public class LoggedQueueable implements Queueable, Finalizer {
    public void execute(QueueableContext context) {
        QueueableLogger.setJobId(context.getJobId());
        QueueableLogger.info('Starting queueable execution');
        
        try {
            // Your processing logic
            QueueableLogger.info('Processing completed successfully');
        } catch (Exception e) {
            QueueableLogger.error('Processing failed: ' + e.getMessage());
            throw e;
        }
    }
    
    public void execute(FinalizerContext context) {
        QueueableLogger.info('Finalizer executing');
        QueueableLogger.flushLogs(); // Ensure all logs are saved
    }
}

Common Pitfalls and How to Avoid Them

1. Mixed DML Operations

// WRONG - This will cause Mixed DML error
public class ProblematicQueueable implements Queueable {
    public void execute(QueueableContext context) {
        User newUser = new User(/*user fields*/);
        insert newUser; // Setup object
        
        Account newAccount = new Account(Name = 'Test');
        insert newAccount; // Non-setup object - MIXED DML ERROR!
    }
}

// CORRECT - Separate setup and non-setup operations
public class CorrectQueueable implements Queueable {
    private Boolean isSetupOperation;
    
    public CorrectQueueable(Boolean setupOp) {
        this.isSetupOperation = setupOp;
    }
    
    public void execute(QueueableContext context) {
        if (isSetupOperation) {
            // Handle setup objects (User, Profile, etc.)
            User newUser = new User(/*user fields*/);
            insert newUser;
            
            // Chain non-setup operation
            System.enqueueJob(new CorrectQueueable(false));
        } else {
            // Handle non-setup objects
            Account newAccount = new Account(Name = 'Test');
            insert newAccount;
        }
    }
}

2. Infinite Chaining Prevention

public class SafeChainQueueable implements Queueable {
    private Integer chainDepth = 0;
    private static final Integer MAX_CHAIN_DEPTH = 100;
    
    public SafeChainQueueable(Integer depth) {
        this.chainDepth = depth;
    }
    
    public void execute(QueueableContext context) {
        // Prevent infinite chaining
        if (chainDepth >= MAX_CHAIN_DEPTH) {
            System.debug('Maximum chain depth reached, terminating chain');
            return;
        }
        
        // Your processing logic
        
        // Safe chaining with depth tracking
        if (shouldContinueChaining()) {
            SafeChainQueueable nextJob = new SafeChainQueueable(chainDepth + 1);
            System.enqueueJob(nextJob);
        }
    }
    
    private Boolean shouldContinueChaining() {
        // Your logic to determine if chaining should continue
        return true; // Placeholder
    }
}

Real-World Use Cases

1. Large Data Migration

Use queueable classes for migrating large datasets from legacy systems while maintaining system performance and providing detailed progress tracking.

2. Complex Integration Workflows

Implement multi-step integration processes that require sequential API calls to external systems with proper error handling and retry mechanisms.

3. Bulk Data Processing

Process large volumes of records with complex business logic that would exceed synchronous processing limits.

4. Scheduled Cleanup Operations

Perform regular maintenance tasks like archiving old records, cleaning up temporary data, or optimizing data structures.

Conclusion

Queueable Apex provides a powerful and flexible framework for asynchronous processing in Salesforce. By understanding and implementing the concepts covered in this guide, you can:

  • Build scalable applications that handle large data volumes efficiently
  • Implement robust error handling with the Finalizer interface
  • Create complex workflows using queueable chaining
  • Monitor and debug your asynchronous processes effectively
  • Avoid common pitfalls that can cause processing failures

Must Read

Uday Bheemarpu
Uday Bheemarpu
Articles: 9

Newsletter Updates

Enter your email address below and subscribe to our newsletter

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading