Caching is safe only when you (1) key by the full dependency set, (2) gate reads by a revision/stale check, (3) invalidate only the correct scope, and (4) handle async failures with rollback + bounded retries.
Standards
Practical pattern (keyed async lookup with TTL + backoff)
class KeyedLookup<T> {
private key?: string;
private value?: T;
private pending?: Promise<void>;
private failedAt?: number;
constructor(
private readonly now: () => number,
private readonly onLoaded: () => void,
private readonly retryMs: number,
) {}
async get(key: string, load: () => Promise<T>, refresh = false): Promise<T | undefined> {
this.start(key, load, refresh);
await this.pending;
return this.value;
}
private start(key: string, load: () => Promise<T>, refresh: boolean) {
if (this.key !== key || refresh) {
this.key = key;
this.value = undefined;
this.pending = undefined;
this.failedAt = undefined;
}
if (this.value !== undefined) return;
if (this.pending) return;
if (this.failedAt !== undefined && this.now() - this.failedAt < this.retryMs) return;
const pending = load().then(
(v) => {
if (this.pending !== pending) return;
this.pending = undefined;
this.value = v;
this.onLoaded();
},
() => {
if (this.pending !== pending) return;
this.pending = undefined;
this.failedAt = this.now();
}
);
this.pending = pending;
}
}
Checklist before landing a cache change