Cache only when you can prove the cached value still matches the current “identity” of the request/state, and always have a bounded strategy for refresh/retry.
Standards
Example pattern (key + late-response guard + backoff retry)
type CacheEntry<T> = { key: string; value?: T; pending?: Promise<T>; failedAt?: number };
const FAILURE_RETRY_MS = 30_000;
class KeyedCache<T> {
private entry: CacheEntry<T> = { key: "" };
async get(key: string, load: () => Promise<T>): Promise<T | undefined> {
// Cache identity changed => reset.
if (this.entry.key !== key) {
this.entry = { key };
}
// Success already known.
if (this.entry.value !== undefined) return this.entry.value;
// Backoff after failure.
if (this.entry.failedAt !== undefined && Date.now() - this.entry.failedAt < FAILURE_RETRY_MS) {
return undefined;
}
// De-dupe concurrent loads.
if (this.entry.pending) return this.entry.value;
const pending = load().then(
(value) => {
// Late-response guard: only commit if identity still matches.
if (this.entry.key === key) this.entry = { key, value };
return value;
},
() => {
if (this.entry.key === key) this.entry.failedAt = Date.now();
return undefined;
}
);
this.entry.pending = pending;
return undefined;
}
}
Apply these rules consistently across memory caches, persisted caches, and distributed caches to prevent stale data, unblock recovery from transient/opaque failures, and keep UI/state projections coherent.