Platform Cache Patterns in Salesforce — Safe Caching & Invalidation
Salesforce Platform Cache (Org & Session) is an easy win when you want faster reads, fewer API calls, and smoother traffic spikes—without spinning up your own cache layer. The key is to use safe, predictable patterns (cache-aside/read-through, versioned keys, event-driven invalidation) so you don’t accidentally serve stale or inconsistent data.
Core Concepts (with quick examples + code snaps)
Cache-Aside (a.k.a. lazy loading)
What it is: Check the cache first. If there’s a miss, load from the source (SOQL or a callout), then store the result back in cache. It’s simple, explicit, and works almost everywhere.
Real-world example: Product tiles on a community page. You fetch each Product2 by Id and cache it so repeat views are instant.
public with sharing class ProductCache {
private static final String PART = 'Products'; // Org cache partition name
public static Product2 getById(Id productId) {
String key = 'p:' + (String)productId;
Product2 p = (Product2) Cache.Org.get(PART, key);
if (p != null) return p;
p = [SELECT Id, Name, Family, Description FROM Product2 WHERE Id = :productId LIMIT 1];
Cache.Org.put(PART, key, p); // cache the hydrated SObject
return p;
}
}
Safe Invalidation — versioned keys
What it is: Instead of hunting down and deleting many keys, you increment a version that becomes part of every cache key. Old entries naturally fall behind and age out.
Real-world example: Merch updates pricing rules. You bump the “catalog” version and all consumers start reading fresh data.
public with sharing class CacheVersioning {
private static final String PART = 'Products';
// Read the current version number (or 1 if missing)
public static Integer current(String scope) {
Integer v = (Integer) Cache.Org.get(PART, 'v:' + scope);
return v == null ? 1 : v;
}
// Atomically bump version (best-effort)
public static Integer bump(String scope) {
Integer v = current(scope) + 1;
Cache.Org.put(PART, 'v:' + scope, v);
return v;
}
public static String key(String scope, String rawKey) {
return 'v' + current(scope) + ':' + rawKey;
}
}
// Usage in cache-aside:
public with sharing class CatalogCache {
private static final String PART = 'Products';
public static List<Product2> listByFamily(String family) {
String key = CacheVersioning.key('catalog', 'fam:' + family);
List<Product2> cached = (List<Product2>) Cache.Org.get(PART, key);
if (cached != null) return cached;
List<Product2> rows = [SELECT Id, Name, Family FROM Product2 WHERE Family = :family ORDER BY Name LIMIT 200];
Cache.Org.put(PART, key, rows);
return rows;
}
}
Invalidate everything: Call CacheVersioning.bump('catalog') (from an admin action, a flow, or a post-deploy step) instead of chasing individual keys.
Event-Driven Invalidation (multi-node friendly)
What it is: Use a Platform Event to broadcast either a version bump or a specific key removal. Every app server hears the message and converges on the same state.
Real-world example: An ERP updates the Pricebook; a Platform Event fires and bumps the catalog version across all nodes.
// Setup: In the partition, map key prefix "cfg:" to this builder class.
public class StoreConfigBuilder implements Cache.CacheBuilder {
public Object load(String key) {
// Key example: "cfg:store"
Store_Settings__mdt s = Store_Settings__mdt.getInstance('Default');
Map<String,Object> cfg = new Map<String,Object>{
'currency' => s.DefaultCurrency__c,
'locale' => s.Locale__c
};
return cfg;
}
}
// Caller code (miss triggers StoreConfigBuilder.load automatically)
public with sharing class StoreConfig {
public static Map<String,Object> get() {
return (Map<String,Object>) Cache.Org.get('Products', 'cfg:store');
}
}
Session Cache — per-user, short-lived state
What it is: Cache.Session is scoped to a single user session. It’s perfect for chatty UI bits, wizards, and small ephemeral states.
Real-world example: A multi-step wizard saves a selection so you don’t re-query between screens.
public with sharing class WizardState {
public static void saveDraft(Set<Id> selectedProductIds) {
Cache.Session.put('draft:sel', selectedProductIds);
}
public static Set<Id> loadDraft() {
Set<Id> s = (Set<Id>) Cache.Session.get('draft:sel');
return s == null ? new Set<Id>() : s;
}
}
Soft TTL & Negative Caching (avoid stampedes)
What it is: Platform Cache doesn’t give you a per-item TTL in Apex, so you store a timestamp alongside the value and treat it as a soft TTL. Also cache misses briefly so you don’t hammer the database for nonexistent records.
Real-world example: A product was deleted. Caching the “not found” result prevents repeat queries during the next few minutes.
Write-Through (update DB → refresh cache right away)
What it is: After a successful write, update the relevant cache key (or bump the version) so readers immediately see fresh data.
Real-world example: An admin edits a product name; you refresh the single-item key and bump the family list to keep everything aligned.
trigger Product2After on Product2 (after update) {
for (Product2 p : Trigger.new) {
String key = CacheVersioning.key('catalog', 'p:' + p.Id);
Cache.Org.put('Products', key, p);
}
CacheVersioning.bump('catalog'); // force list queries to refetch
}

Short Summary
Use cache-aside for quick wins, read-through when you want centralized load logic, and session cache for per-user UX. Keep data correct with versioned keys and event-driven invalidation, and prevent thundering herds with soft TTL and negative caching. These Platform Cache patterns deliver faster pages and sturdier integrations—safely.
