Skip to main content
SFDC Developers
Apex

Stateful Batch Apex: Should You Cache Picklist Values?

Vinay Vernekar · · 5 min read

The Allure of the Stateful Variable

When we write Database.Batchable classes, we are often forced to choose between stateless and stateful execution. The Database.Stateful interface allows us to maintain the values of instance variables across multiple execution chunks. For many developers, this is a siren song: why query the same metadata—like picklist values or custom settings—a thousand times when you can query it once in the start method and hold it in a variable for the duration of the job?

In this guide, we’ll analyze whether keeping picklist values in a stateful batch is a brilliant optimization or an architectural trap. While the performance benefits of minimizing SOQL queries are clear, the risks to data integrity and heap size limits are significant.

The Mechanics of Stateful Batch

When a batch class implements Database.Stateful, Salesforce serializes the object's instance variables at the end of every execute method and deserializes them at the beginning of the next.

If we decide to cache picklist values—perhaps fetching them from Schema.DescribeFieldResult and mapping them to a Map<String, List<String>>—we are increasing the serialized footprint of our object. Here is the standard way developers typically attempt this:

global class PicklistCacheBatch implements Database.Batchable<SObject>, Database.Stateful {
    private Map<String, List<String>> picklistCache = new Map<String, List<String>>();

    global PicklistCacheBatch() {
        // Populating the cache once
        Schema.DescribeFieldResult fieldResult = Account.Industry.getDescribe();
        List<Schema.PicklistEntry> ple = fieldResult.getPicklistValues();
        List<String> values = new List<String>();
        for(Schema.PicklistEntry f : ple) {
            values.add(f.getValue());
        }
        picklistCache.put('Industry', values);
    }

    global void execute(Database.BatchableContext BC, List<Account> scope) {
        // Using the stateful map
        for(Account a : scope) {
            if(picklistCache.get('Industry').contains(a.Industry)) {
                // Process logic
            }
        }
    }
}

While this looks efficient, we have to consider what happens if our picklistCache grows large. Every time the batch chunk finishes, this entire map is serialized into the database. If you are handling large batches with high concurrency, you might run into heap size or serialization limitations that effectively cripple the batch before it finishes.

The Hidden Costs: Memory and Serialization

There are three primary reasons why caching metadata in a Stateful variable is frequently an anti-pattern:

  1. Serialization Limits: Salesforce has strict limits on the size of the stateful object. If your picklist data is massive or if you are caching many fields, your object will exceed the serialization limit, resulting in a "Serializing stateful batch failed" error.
  2. Heap Size: Since the object persists across execution chunks, the memory remains allocated throughout the lifetime of the batch. This eats into your total heap allocation, potentially leaving less room for the actual processing logic within the execute method.
  3. Stale Data: Batch jobs can take a long time to complete. If a metadata change occurs (e.g., a picklist value is deactivated) while the batch is running, your cached map will contain stale, incorrect information.

When is Caching Actually Appropriate?

If we decide that standard schema calls are too heavy, we should look at alternatives. Schema methods are generally very fast because they are cached by the platform at the system level.

In most scenarios, calling getDescribe() inside the execute method is significantly safer and cheaper than many developers realize. The platform performance overhead of calling getDescribe() is often lower than the overhead of serializing and deserializing a large stateful object.

However, if you are performing complex calculations on thousands of picklist values per record, you might consider a Static Map rather than a Stateful instance variable:

public class PicklistHelper {
    private static Map<String, List<String>> cachedMap;

    public static List<String> getIndustryValues() {
        if (cachedMap == null) {
            cachedMap = new Map<String, List<String>>();
            // Fill the map
        }
        return cachedMap.get('Industry');
    }
}

Using a static variable (in a separate utility class) allows for a "lazy load" pattern. It caches the data for the duration of the transaction (the current chunk), which is often exactly what you need without the baggage of Database.Stateful.

Expert Recommendations

For the vast majority of Salesforce implementations, avoid storing picklist values or other metadata in Database.Stateful variables. The platform's built-in schema caching is highly optimized.

  • Avoid Stateful for Metadata: Only use Database.Stateful for counters, error logs, or aggregate summaries that are strictly necessary to pass between chunks.
  • Prefer Lazy Loading: If performance is a concern, use a static utility class to cache data within the execution scope of a single chunk.
  • Evaluate Query Complexity: If you are worried about the performance of your Describe calls, use System.Limits.getHeapSize() in your test classes to monitor the impact.

Key Takeaways

  • Stateful is for State: Reserve Database.Stateful for variables that track the state of the batch execution, such as row counts or processed IDs.
  • Metadata is Fast: Schema.DescribeFieldResult is highly efficient; do not treat it like a traditional SOQL query that requires manual caching.
  • Serialization Overhead: Storing large collections in stateful variables adds significant serialization/deserialization time to every single batch chunk, potentially increasing total execution time rather than decreasing it.
  • Static Utility Pattern: For per-chunk optimization, use a static variable in a helper class to prevent re-fetching data while avoiding the pitfalls of persisting state across transactions.

Share this article

Get weekly Salesforce dev tutorials in your inbox

Comments

Loading comments...

Leave a Comment

Trending Now