Adopt a single standard for all caches (data, discovery, warm-ups, and load-result caches):
1) Key by the real identity (include all dependencies)
2) Invalidate using an explicit version/revision boundary
3) Define TTL/idle eviction + failure backoff
4) Prevent cache poisoning
5) Validate cache scope at read time
Minimal pattern (identity-keyed lookup with backoff and async safety):
class KeyedLookup<T> {
private key?: string
private value?: T
private pending?: Promise<void>
private failedAt?: number
private generation = 0
constructor(
private readonly now: () => number,
private readonly retryMs: number,
private readonly load: (key: string) => Promise<T>,
private readonly onSettled: () => void,
) {}
async get(key: string, refresh = false): Promise<T | undefined> {
if (this.key !== key || refresh) {
this.generation++
this.key = key
this.value = undefined
this.pending = undefined
this.failedAt = undefined
}
if (this.value !== undefined) return this.value
if (this.pending) return undefined
if (this.failedAt !== undefined && this.now() - this.failedAt < this.retryMs) return undefined
const gen = this.generation
this.pending = this.load(key)
.then((v) => {
if (gen !== this.generation) return
this.value = v
this.failedAt = undefined
this.onSettled()
})
.catch(() => {
if (gen !== this.generation) return
this.failedAt = this.now()
})
.finally(() => {
if (gen === this.generation) this.pending = undefined
})
return undefined
}
}
If you apply this checklist consistently, you’ll avoid stale/late async results, ensure caches refresh when inputs change, keep failures from freezing features, and stop one failing lookup from corrupting shared state.