Key Points at a Glance
- Two Scopes: Org Cache (shared across all users) and Session Cache (tied to one specific user's session).
- Temporary Storage: Items in the cache can be evicted at any time to free up space. Your code must always be prepared to rebuild the data.
- Best Pattern: Use
Cache.CacheBuilderto automatically handle cache misses and rebuild data transparently. - Limits: Cache space is divided into Partitions. You manage capacity based on your org's allocated limits.
Understanding Platform Cache
Platform Cache is managed, in-memory storage designed to make your Salesforce applications run faster. Instead of repeatedly querying the database for data that rarely changes, you stash it in memory. You organize this memory into Partitions to control how much space different applications get.
You interact with the cache via Apex using the Cache.OrgPartition and Cache.SessionPartition classes. However, modern developers prefer the declarative Cache.CacheBuilder interface because it naturally forces you to handle the "load-on-miss" pattern gracefully.
- Rule: Pay once, read many. Cache data that is stable, widely shared, and computationally expensive to build.
- Gain: Microsecond read times instead of burning database queries or API callouts. Saves your transaction limits.
- Reach for: Use Session Cache when the data is unique per user. Use Org Cache when the data is the same for everyone.
- Price: It is a speed layer, not a hard drive. Entries can disappear at any moment, meaning every read must have a fallback plan.
- Limits: Cache partitions have strict memory budgets. Never cache sensitive secrets, and never cache record data that requires strict sharing rule evaluations.
- The Alternative: Querying the database every time. It's always accurate but burns precious SOQL limits on data that might only change once a week.
- Best Practice: Use
CacheBuilder. It standardizes checking the cache and automatically rebuilding it if it's empty. - At Volume: If you set a long Time-To-Live (TTL) without a way to invalidate the cache, your users will be stuck staring at stale data.
Real-Life Example: Paying for the Same Answer Every Click
The Scenario: Every screen in your Lightning console displays today's fuel exchange rates. These rates only change once every 24 hours.
The Old/Bad Way: Every time a user loads a page, Lightning Web Components (LWC) call Apex, which runs a SOQL query to fetch the rates. If 40 users load dozens of pages, you are hammering the database for the exact same answer thousands of times an hour. Pages load slower, and SOQL limits are wasted.
The New/Good Way:
- Create an Org Cache partition.
- Access the data using a
CacheBuilderclass. - When the first user loads the page, the cache misses. The code queries the database, stores the rates in the cache, and sets a TTL of 4 hours.
- For the next 4 hours, thousands of page loads read the rates directly from memory in microseconds.
The Payoff: One SOQL query now feeds thousands of reads. You paid the database cost once, and read it for free everywhere else.
How CacheBuilder Works
Here is what the CacheBuilder pattern looks like in practice. Notice how you only write the logic to build the data. Salesforce handles checking the cache and storing the result.
public class ExchangeRateCache implements Cache.CacheBuilder {
// This method is ONLY called if the cache is empty or expired
public Object doLoad(String key) {
return [SELECT CurrencyIsoCode, ConversionRate
FROM CurrencyType
WHERE IsActive = true];
}
}
// To use it anywhere in your code:
// If it's in cache, it returns instantly. If not, doLoad() runs automatically.
List<CurrencyType> rates = (List<CurrencyType>) Cache.Org.get(ExchangeRateCache.class, 'dailyRates');
Core Q&A and Common Scenarios
A: You want to cache expensive, frequently-read, slowly-changing data. Good examples include org-wide configuration objects, exchange rates, category trees, and complex per-session user context.
You must never cache:
- Data you cannot afford to lose (since the cache is evictable).
- Rapidly-changing transactional data (to avoid stale data issues).
- Unencrypted secrets or passwords (use Protected Custom Metadata or Named Credentials instead).
CacheBuilder wins because it makes handling a cache miss structural. By implementing the doLoad(key) method, every read either hits the cache or transparently rebuilds it. This eliminates the classic bug where a developer forgets to write a fallback query for a cache miss.
A: Use Session Cache. Because the data is highly specific to the logged-in user, putting it in Org Cache would require complex, user-ID-suffixed keys and would burn through your shared partition limit on data only one person cares about. Session cache naturally scopes to the user and dies when they log out, which automatically limits how stale the data can get.
Follow-up Trap: Session cache is not available in asynchronous Apex (like Batchable or Queueable) because there is no active browser session. If you need this matrix in a Queueable, you must either recompute it, or temporarily use Org Cache with user-suffixed keys and manage the invalidation yourself.
A: The cache was likely deployed with a long Time-To-Live (TTL) but no invalidation strategy. There are three ways to fix this:
- Event-Driven Invalidation: Whenever a record changes, use an Apex Trigger to explicitly remove or refresh the cached keys using
Cache.Org.remove(). You can also publish a Platform Event to handle this asynchronously. - Version-Stamped Keys: Include a version number in your cache key. When data changes, bump the version number. Stale entries are simply never requested again and age out naturally.
- Honest TTLs: Match your TTL to the actual business tolerance for stale data. If the business can't handle data being older than 5 minutes, set a 5-minute TTL. The golden rule is: A caching design that only adds data but never invalidates it is an unfinished design.
A: By default, cache hits always return null in test classes to ensure test isolation. If you are specifically trying to test your caching logic, you must manually populate the cache within the test method using Cache.Org.put(), or better yet, structure your code so that test methods focus on testing the doLoad() miss-handling logic to ensure data rebuilds correctly.