Understanding Database.Stateful in Salesforce Batch Apex

Understanding Database.Stateful in Salesforce Batch Apex

Batch Apex in Salesforce is a powerful tool for processing large amounts of data asynchronously. In this blog post, we’ll explore how to use Database.Stateful to maintain state across the different transactions of a batch job. Specifically, we’ll:

  1. Explain the Database.Stateful interface.
  2. Demonstrate how to track Database.insert and Database.update successes and failures.
  3. Show an example of how to email success and failure results. Format these results as a CSV file. Send this file to the admin in the finish method.

What is Database.Stateful?

  • Stateless means that all the instance variables do not retain their values. This happens between the multiple executions of the execute() method of each batch.
  • Implementing the Database.Stateful interface enables a batch job to preserve instance variable values. This makes it possible to accumulate results.
  • You can maintain running totals or carry forward any context across multiple execute method invocations during the job’s lifecycle.

Stateful vs. Stateless Batch Apex

In Batch Apex, the terms Stateful and Stateless indicate different behaviors. Stateful means the batch job maintains its state between multiple executions of the execute() method. Stateless means it does not.

Stateless Batch Apex
  • Default Behavior:
    • Each execution of the execute method receives a fresh instance of the class, and all instance variables are reset.
  • Best Suited For:
    • Independent processing where no context from previous batches is needed.
Stateful Batch Apex
  • Database.Stateful:
    • Retains the state of instance variables across multiple executions of the execute method and into the finish method.
  • Use Cases:
    • Summarizing data, maintaining running totals, or aggregating results across batches.
    • Capturing successes and failures across the job for reporting.
  • Drawbacks:
    • Performance Impact:
      • As the class will be serialized at the end of each execute method to update its internal state.This extra serialization results in longer execution time.
    • Heap Usage:
      • Retaining too much data in instance variables can lead to heap size governor limits.

Decision-Making: When Do You Need Database.Stateful?

Ask yourself these questions, when you want to decide to use stateful in your batch class:

  1. “Does one execute method need data from a previous execute method?”
  2. “Does the finish method need data collected across all executions?”
  • If the answer is ‘Yes’:
    Use Database.Stateful to retain state across transactions.
  • If the answer is ‘No’:
    Stick to the default stateless behavior to optimize performance.

Best Practices When Using Database.Stateful

  1. Avoid Static Variables:
    • Static variables are reset between batch executions, even in stateful classes.
  2. Minimize Data Retention:
    • Retain only essential information to avoid excessive memory usage.
  3. Consider Alternatives:
    • Explore using Custom Settings, Custom Metadata, or temporary objects to store state if performance is critical.
  4. Test for Heap Usage:
    • Test the batch class with large datasets to ensure it doesn’t exceed heap limits.
  5. Use Sparingly:
    • Most batch jobs do not require Database.Stateful. Use it only when necessary for your specific use case.

Batch Apex Examples with Stateful Variables and Error Handling

Scenario Overview

Both examples demonstrate batch processing with error tracking and email notifications, commonly used for:

  • Mass data updates/insertions
  • Data cleanup operations
  • Compliance reporting
  • Audit trails
Example 1: Account Update Batch

This batch updates Account descriptions and tracks successes/failures.

Code Structure and Explanation
/**
* @description – This batch class updates the description of Account records created today.
* It tracks success and error records and sends an email with the results as CSV attachments.
*/
public class AccountUpdateBatch implements Database.Batchable<sObject>, Database.Stateful {
// Stateful variables to track results
private List<String> successRecords;
private List<String> errorRecords;
/**
* @description – Constructor to initialize lists for success and error records.
*/
public AccountUpdateBatch() {
successRecords = new List<String>();
errorRecords = new List<String>();
}
/**
* @description – Start method to define the scope of records to process.
* This method queries for Account records created today.
* @param – BC The batchable context.
* @return – A QueryLocator for the records to process.
*/
public Database.QueryLocator start(Database.BatchableContext BC) {
return Database.getQueryLocator('SELECT Id, Name, Description FROM Account WHERE CreatedDate = TODAY');
}
/**
* @description Execute method to process each batch of records.
* This method updates the description of each account in the scope.
* It also tracks success and error records.
* @param – BC The batchable context.
* @param – scope The list of records to process in this batch.
* @return – void
*/
public void execute(Database.BatchableContext BC, List<Account> scope) {
// declare lists to hold records for update
List<Account> accountsToUpdate = new List<Account>();
// Loop through each account in the scope
for(Account acc : scope) {
// Update the description field
acc.Description = 'Updated by batch – ' + System.today();
// Add to the list of accounts to update
accountsToUpdate.add(acc);
}
// Perform the update operation using Database.update to handle partial success
// This allows us to capture success and error records
List<Database.SaveResult> srList = Database.update(accountsToUpdate, false);
// Loop through the results and categorize them
for(Integer i = 0; i < srList.size(); i++) {
// Check if the update was successful
if(srList[i].isSuccess()) {
// Add to success records
successRecords.add(accountsToUpdate[i].Id + ',' + accountsToUpdate[i].Name);
} else {
// Add to error records
errorRecords.add(accountsToUpdate[i].Id + ',' + accountsToUpdate[i].Name + ',' + srList[i].getErrors()[0].getMessage());
}
}
}
/**
* @description – Finish method to send email with results.
* This method is called after all batches are processed.
* It sends an email with CSV attachments for success and error records.
* @param – BC The batchable context.
* @return – void
*/
public void finish(Database.BatchableContext BC) {
// Create CSV strings for success with headers
String successCSV = 'Id,Name\n' + String.join(successRecords, '\n');
// Create CSV strings for error with headers
String errorCSV = 'Id,Name,Error\n' + String.join(errorRecords, '\n');
// Create email message
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
// Set the email to address
mail.setToAddresses(new String[] {'admin@example.com'});
// Set the email subject
mail.setSubject('Account Update Batch Results');
// Set the email plain text body
mail.setPlainTextBody('Please find attached the results of the Account Update Batch job.');
// Declare a list to hold file attachments
List<Messaging.EmailFileAttachment> attachments = new List<Messaging.EmailFileAttachment>();
// Create file attachments for success records
Messaging.EmailFileAttachment successAttach = new Messaging.EmailFileAttachment();
// Set the file name for success records
successAttach.setFileName('success_records.csv');
// Set the body of the attachment to the CSV string
successAttach.setBody(Blob.valueOf(successCSV));
// Add the success attachment to the list
attachments.add(successAttach);
// Create file attachments for error records
Messaging.EmailFileAttachment errorAttach = new Messaging.EmailFileAttachment();
// Set the file name for error records
errorAttach.setFileName('error_records.csv');
// Set the body of the attachment to the CSV string
errorAttach.setBody(Blob.valueOf(errorCSV));
// Add the error attachment to the list
attachments.add(errorAttach);
// Set the attachments to the email
mail.setFileAttachments(attachments);
// Send the email
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
How It Works:
  1. Stateful Variables
    • successRecords: Maintains list of successful updates across batch executions
    • errorRecords: Tracks failed updates with error messages
  2. Batch Methods
    • start(): Queries accounts created today
    • execute():
      • Updates account descriptions
      • Uses Database.update with partial success (allOrNone=false)
      • Records successes and failures in stateful lists
    • finish():
      • Converts tracking lists to CSV format
      • Sends email with attachments to admin
Example 2: Contact Creation Batch

This batch creates contacts for recent accounts and tracks the results using a wrapper class.

Code Structure and Explanation
/**
* @description – This batch class createS contacts for accounts created in the last 7 days.
* It tracks success and error records and sends an email with the results as CSV attachments.
*/
public class ContactCreationBatch implements Database.Batchable<sObject>, Database.Stateful {
// Stateful variables to track results
private List<ContactWrapper> successList;
private List<ContactWrapper> failureList;
/**
* @description – Constructor to initialize lists for success and error records.
*/
public ContactCreationBatch() {
successList = new List<ContactWrapper>();
failureList = new List<ContactWrapper>();
}
/**
* @description – Start method to define the scope of records to process.
* This method queries for Account records created in the last 7 days.
* @param – BC The batchable context.
* @return – A QueryLocator for the records to process.
*/
public Database.QueryLocator start(Database.BatchableContext BC) {
return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE CreatedDate = LAST_N_DAYS:7');
}
/**
* @description – Execute method to process each batch of records.
* This method creates contacts for each account in the scope.
* It also tracks success and error records.
* @param – BC The batchable context.
* @param – scope The list of records to process in this batch.
* @return – void
*/
public void execute(Database.BatchableContext BC, List<Account> scope) {
// Declare lists to hold records for insert
List<Contact> contactsToInsert = new List<Contact>();
// Loop through each account in the scope
for(Account acc : scope) {
// Create a new contact for each account
Contact con = new Contact(
AccountId = acc.Id,
LastName = acc.Name + ' Contact',
Email = 'contact_' + acc.Id + '@example.com'
);
// Add to the list of contacts to insert
contactsToInsert.add(con);
}
// Perform the insert operation using Database.insert to handle partial success
// This allows us to capture success and error records
List<Database.SaveResult> srList = Database.insert(contactsToInsert, false);
// Loop through the results and categorize them
for(Integer i = 0; i < srList.size(); i++) {
// Check if the insert was successful
if(srList[i].isSuccess()) {
// Add to success records
successList.add(new ContactWrapper(
scope[i].Id,
contactsToInsert[i].LastName,
'Success',
''
));
} else {
// Add to error records
failureList.add(new ContactWrapper(
scope[i].Id,
contactsToInsert[i].LastName,
'Failed',
srList[i].getErrors()[0].getMessage()
));
}
}
}
/**
* @description – Finish method to perform actions after all batches are processed.
* This method sends an email with the results of the batch job.
* It includes CSV attachments for success and failure records.
* @param – BC The batchable context.
* @return – void
*/
public void finish(Database.BatchableContext BC) {
// Create CSV header
String csvHeader = 'AccountId,ContactName,Status,ErrorMessage\n';
// Create CSV strings for success records
List<String> successCSVRows = new List<String>();
// Create CSV strings for failure records
List<String> failureCSVRows = new List<String>();
// Loop through success and failure lists to create CSV rows
for(ContactWrapper sw : successList) {
// Add success records to CSV rows
successCSVRows.add(sw.toCSVString());
}
// Loop through failure list to create CSV rows
for(ContactWrapper fw : failureList) {
// Add failure records to CSV rows
failureCSVRows.add(fw.toCSVString());
}
String successCSV = csvHeader + String.join(successCSVRows, '\n');
String failureCSV = csvHeader + String.join(failureCSVRows, '\n');
// Send email with results
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new String[] {'admin@example.com'});
email.setSubject('Contact Creation Batch Results');
email.setPlainTextBody('Batch job completed. Please find the results attached.');
// Create attachments
List<Messaging.EmailFileAttachment> attachments = new List<Messaging.EmailFileAttachment>();
if(!successList.isEmpty()) {
Messaging.EmailFileAttachment successAttach = new Messaging.EmailFileAttachment();
successAttach.setFileName('success_contacts.csv');
successAttach.setBody(Blob.valueOf(successCSV));
attachments.add(successAttach);
}
if(!failureList.isEmpty()) {
Messaging.EmailFileAttachment failureAttach = new Messaging.EmailFileAttachment();
failureAttach.setFileName('failed_contacts.csv');
failureAttach.setBody(Blob.valueOf(failureCSV));
attachments.add(failureAttach);
}
email.setFileAttachments(attachments);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { email });
}
/**
* @description – ContactWrapper class to hold contact creation results.
* This class is used to track success and error records.
* It contains fields for account ID, contact name, status, and error message.
* It also provides a method to convert the object to a CSV string.
*/
private class ContactWrapper {
String accountId;
String contactName;
String status;
String errorMessage;
/**
* @description – Constructor to initialize ContactWrapper object.
* @param – accId The account ID.
* @param – name The contact name.
* @param – sts The status of the contact creation.
* @param – err The error message if any.
*/
public ContactWrapper(String accId, String name, String sts, String err) {
accountId = accId;
contactName = name;
status = sts;
errorMessage = err;
}
/**
* @description – Method to convert ContactWrapper object to CSV string.
* @return – A CSV string representation of the object.
*/
public String toCSVString() {
return String.join(new List<String>{accountId, contactName, status, errorMessage}, ',');
}
}
}
How It Works:
  1. Wrapper Class
    • Encapsulates contact creation details
    • Provides CSV formatting method
    • Stores:
      • Account ID
      • Contact Name
      • Status
      • Error Message
  2. Batch Processing
    • start(): Queries accounts from last 7 days
    • execute():
      • Creates new contacts
      • Uses Database.insert with partial success
      • Stores results in wrapper objects
    • finish():
      • Generates separate CSVs for successes and failures
      • Attaches CSVs to admin email
Key Features in Both Examples:
  1. State Maintenance with Database.Stateful:
    • We use instance variables successRecords and failureRecords to maintain state across batches.
  2. Capturing Save Results:
    • The Database.update method with false allows partial success. We iterate through Database.SaveResult to segregate successes and failures.
  3. CSV Generation:
    • The CSV is constructed as a string with comma-separated values.
  4. Emailing Results:
    • The Messaging.SingleEmailMessage class is used to email the CSV as an attachment.

Summary

Using Database.Stateful in a batch class enables you to maintain state and capture cumulative results across multiple executions. By combining this capability with Database.SaveResult and the Salesforce email API, you can effectively monitor and report the outcomes of your batch jobs. This approach ensures transparency and allows administrators to quickly address any issues highlighted in the failure logs.

Start using Database.Stateful in your batch classes today to handle complex, stateful operations with ease!

References

One response to “Understanding Database.Stateful in Salesforce Batch Apex”

  1. Great post! Very well explained and super helpful for understanding Database.Stateful in Batch Apex.

    The examples are practical and easy to follow. Perfect for real-world use cases. Looking forward to more content like this.

    Like

Leave a reply to shantanu kumar Cancel reply