- The four caching layers in Salesforce and what each one caches
- Platform Cache: org partition vs session partition, TTL configuration, and when to use it
- Lightning Data Service: how it caches record data client-side and when caches go stale
- What is NOT cached — the missing SOQL result cache and its performance implications
- Static resource caching: CDN, browser cache, and version busting strategies
- Cache invalidation patterns and the architectural pitfalls of stale cache data
The Four Caching Layers in Salesforce
When Salesforce architects discuss caching, they are almost never talking about a single mechanism. Salesforce caching operates across four distinct layers, each with different scope, lifetime, invalidation behaviour, and use cases. Conflating these layers is one of the most common sources of performance misconceptions and stale data bugs.
Layer 1 — Platform Cache: A server-side key-value cache managed by Apex or Flow. Stores serialised Apex objects, strings, or arbitrary data. Has org-scoped (shared across all sessions) and session-scoped (per user session) partitions. TTL is configurable per cache put operation. This is the layer developers control explicitly.
Layer 2 — Lightning Data Service (LDS): A client-side record cache managed by the Lightning framework. LDS caches the field values of Salesforce records accessed by Lightning components and shares them across components on the same page. Changes to records via LDS automatically propagate to all LDS consumers on the page. This is implicit caching managed by the framework.
Layer 3 — Static Resource Cache: CSS, JavaScript, and image files served as Salesforce static resources are cached by the browser and the Salesforce CDN. This is a standard HTTP caching layer — no Salesforce-specific behaviour, but with version busting implications for deployments.
Layer 4 — Salesforce Internal Infrastructure Caching: Metadata, security configuration, and system setup data is cached within the Salesforce application servers. This is invisible to developers but explains why metadata changes (new fields, permission changes) sometimes require a few minutes to propagate across all platform nodes.
Platform Cache: The Developer-Controlled Layer
Platform Cache is the primary tool for Apex developers to reduce repeated expensive operations — SOQL queries, callouts, complex calculations — by storing their results and retrieving them on subsequent calls within the cache's TTL.
Platform Cache has two partition types with different scope and lifetime:
Org Partition: Data stored in the org partition is accessible to any Apex code running in the org, across all user sessions. This is appropriate for data that is the same for all users: reference data (static lookup values, configuration tables), external API responses that don't vary per user, and computationally expensive aggregations that are shared across users. TTL can be set from 1 second to 48 hours.
Session Partition: Data stored in the session partition is accessible only within the current user's session. This is appropriate for user-specific data: personalisation preferences, multi-step form state, permission-denormalised data specific to the current user's access context. Session cache is automatically invalidated when the session ends (logout or expiry).
// Platform Cache — storing and retrieving from org partition
public class ConfigCacheService {
private static final String CACHE_KEY = 'configData';
private static final Integer TTL_SECONDS = 3600; // 1 hour
public static Map<String, String> getConfig() {
Cache.OrgPartition orgPart = Cache.Org.getPartition('local.AppConfig');
// Try cache first
Map<String, String> config =
(Map<String, String>) orgPart.get(CACHE_KEY);
if (config == null) {
// Cache miss — query from database
config = loadConfigFromDatabase();
orgPart.put(CACHE_KEY, config, TTL_SECONDS);
}
return config;
}
// Call this when config changes to force cache refresh
public static void invalidateCache() {
Cache.OrgPartition orgPart = Cache.Org.getPartition('local.AppConfig');
orgPart.remove(CACHE_KEY);
}
}
Lightning Data Service: The Client-Side Record Cache
Lightning Data Service (LDS) is the framework layer that manages record data in Lightning components. When a Lightning Web Component uses @wire(getRecord) or @wire(updateRecord), LDS intercepts the call and manages a client-side cache of the record's field values.
The key architectural behaviour of LDS is shared cache across components on the same page. If three components on the same record page all use @wire(getRecord) for the same record, LDS makes one server call and shares the result across all three components. If any component updates the record via LDS, the cache is automatically invalidated and all components on the page receive the updated data — without explicit coordination code.
LDS also handles optimistic UI updates — when you call updateRecord via LDS, the UI can update immediately with the new values before the server confirms the save. If the server save fails, LDS rolls back the optimistic update. This makes Lightning interfaces feel responsive without custom "loading" state management.
// Lightning Web Component using LDS wire adaptor
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';
import ANNUAL_REVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue';
export default class AccountSummary extends LightningElement {
@api recordId;
@wire(getRecord, {
recordId: '$recordId',
fields: [ACCOUNT_NAME_FIELD, ANNUAL_REVENUE_FIELD]
})
account;
// LDS caches this record — other components on the page
// sharing this recordId receive the same cached data
get name() {
return getFieldValue(this.account.data, ACCOUNT_NAME_FIELD);
}
}
What Is NOT Cached: The SOQL Non-Cache Reality
Understanding what Salesforce does NOT cache is as important as understanding what it does cache — because many performance optimisation attempts are built on a false assumption about platform-level caching that doesn't exist.
SOQL query results: The most important non-cache. Salesforce has no query result cache. Executing the same SOQL query twice in the same transaction, in two different Apex methods, or in two different transactions all result in separate database reads. The second call to [SELECT Id FROM Account WHERE ...] queries the database fresh — there is no "already ran this" cache.
Aggregate results: COUNT(), SUM(), AVG() and other aggregate SOQL queries are similarly uncached. Large COUNT() queries on multi-million-record objects run against the full database on every call. This is why replacing COUNT() queries in frequently-called Apex with Platform Cache values is one of the most impactful performance optimisations available.
External callout responses: Callout results are never cached by Salesforce. Every HTTP callout to an external endpoint is a new network request. If you call a reference data API 50 times in a transaction, you make 50 callout attempts — subject to callout limits and external API rate limits. Cache callout responses in Platform Cache when the data is stable within the cache TTL window.
Static Resource Caching and Version Management
Salesforce static resources (CSS files, JavaScript libraries, images) are served with standard HTTP caching headers. Browsers cache these files locally and the Salesforce CDN caches them at edge nodes. This is a significant performance benefit — a user who has visited a Salesforce org recently does not re-download static resources on every page load.
The problem this creates for deployments: when you update a static resource (update a JavaScript library, change a CSS file), browsers and CDN nodes that have the old version cached may continue serving the stale version until the cache TTL expires. For users who don't clear their browser cache, they may see old styling or broken JavaScript behaviour after a deployment.
Salesforce addresses this via versioned static resource URLs. Each static resource version has a unique URL that includes a version hash. Deploying a new version of a static resource creates a new URL — browser and CDN caches treat it as a new resource and request it fresh. Old cached versions of the old URL remain cached but are no longer referenced by updated pages.
The implication for LWC components: static resource version references in component HTML are automatically updated when the resource is redeployed, so the version busting is automatic. For manually referenced static resources in Visualforce or custom HTML, the version URL must be regenerated after each resource update — using $Resource.myStaticResource in Visualforce ensures the versioned URL is always used.
Cache Invalidation Patterns and Stale Data Pitfalls
Cache invalidation — knowing when cached data is no longer valid and must be refreshed — is the hardest part of working with Platform Cache. The failure mode is stale data: users see outdated values because the cache has not been invalidated after the underlying data changed.
The two primary cache invalidation patterns in Salesforce are TTL-based expiry (the cache expires automatically after the configured TTL) and explicit invalidation (the cache is explicitly removed when the source data changes).
TTL-based expiry is simple to implement but creates a maximum staleness window: users may see data that is up to [TTL] seconds old. This is acceptable for reference data that changes infrequently (postal code lists, country codes, product categories) but unacceptable for business data that must be current (account credit limits, inventory levels, approval status).
Explicit invalidation is more precise but requires that the invalidation event be reliably triggered whenever the source data changes. The standard pattern: an Apex trigger on the source object calls Cache.invalidate() when relevant fields change. This ensures the cache is cleared immediately when data changes, forcing the next reader to fetch fresh data.
// Trigger-based cache invalidation
trigger AccountCacheInvalidator on Account (after update) {
Set<Id> changedIds = new Set<Id>();
for (Account acc : Trigger.new) {
Account old = Trigger.oldMap.get(acc.Id);
// Invalidate if credit limit or status changed
if (acc.Credit_Limit__c != old.Credit_Limit__c ||
acc.Account_Status__c != old.Account_Status__c) {
changedIds.add(acc.Id);
}
}
if (!changedIds.isEmpty()) {
Cache.OrgPartition orgPart =
Cache.Org.getPartition('local.CreditData');
for (Id accountId : changedIds) {
orgPart.remove('credit_' + accountId);
}
}
}
Key Takeaways
- Salesforce caching operates across four layers: Platform Cache (developer-controlled server-side), Lightning Data Service (client-side record cache), static resource cache (browser/CDN), and internal infrastructure cache (metadata, invisible to developers).
- Platform Cache has org-scoped partitions (shared across all users, for reference data) and session-scoped partitions (per user, for personalised state). Capacity is licensed — handle CacheException defensively in production code.
- Lightning Data Service caches record fields client-side and shares them across components on the same page, eliminating duplicate server calls and automatically propagating record updates to all consuming components.
- SOQL queries are NOT cached — every execution hits the database. This is the most important caching misconception. Cache SOQL results explicitly in Platform Cache for frequently-called reference queries to avoid repeated database reads.
- Static resource version URLs provide automatic cache busting on deployment. Use $Resource references (not hardcoded paths) to ensure components always reference the current version after deployment.
- Cache invalidation is the hardest problem: TTL-based expiry accepts staleness up to the TTL window; trigger-based explicit invalidation provides immediate freshness at the cost of invalidation event management complexity.
Test Your Understanding
1. An Apex method runs a COUNT() query against a 50-million-record object. The method is called on every page load by 5,000 concurrent users. What is the most impactful performance optimisation?
2. Three LWC components on the same Salesforce record page all use @wire(getRecord) for the same record. How many server-side data requests are made when the page loads?
3. You cached an Account's credit limit in Platform Cache (org partition) with a 4-hour TTL. An admin updates the credit limit in Salesforce 30 minutes later. A transaction checks the cached value 45 minutes after the original cache. What value does it see?
Discussion & Feedback