Stamping Cached Lookups with a Mapping Generation So Stale Answers Expire on Their Own
Integration code often keeps a small in-memory cache of lookups, for example 'supplier code to internal category'. When someone edits that mapping, the cache keeps answering with the old value. This article proposes one conditional design: stamp every cached value with a mapping generation, and treat any value whose stamp is behind the current generation as absent.
The idea
Keep one integer, the mapping generation, that increases by one whenever any mapping row changes. Each cache entry stores the value and the generation that was current when the lookup began. A read is a hit only if the entry's stamp equals the current generation. Otherwise it is a miss and the source is consulted.
Two details matter. First, read the generation before reading the source, and stamp with that earlier number. If you read it afterwards, a change could land in between and a stale value would carry a fresh stamp. Second, bump the generation in the same transaction as the mapping edit, so nobody can see the new mapping with the old number.
Choosing a branch
Branch A: one process owns both the cache and the edits. Bump an in-process counter on edit. Nothing else is needed.
Branch B: several processes share the mapping. Each must learn the current generation from a shared place. Decide a bounded staleness window, for instance 'a process re-reads the generation at most every few seconds', and write it down as the accepted worst case. Do not claim instant invalidation.
Branch C: the generation source cannot be reached. Choose per use. If a wrong category merely mislabels a report, serving stale entries for a short stated period may be acceptable. If a wrong value would feed an external write, fail closed: refuse the lookup and queue the work.
Branch D: you cannot tell which keys a change affects. Invalidate the whole cache through the generation. Only if dependencies are known and reliable consider per-key stamps, and accept the extra bookkeeping.
Worked hypothetical fixture
Setup: generation 7. The cache holds three entries, keys K1, K2, K3, all stamped 7. An editor changes the mapping for K2 from 'Fasteners' to 'Hardware'; generation becomes 8.
| Step | Event | Expected result |
|---|---|---|
| 1 | Read K1 at generation 7 | hit, 0 source reads |
| 2 | A fill for K2 starts, having read generation 7 | in flight |
| 3 | Edit commits, generation 8 | all three entries now stale |
| 4 | The in-flight fill finishes and stores K2 stamped 7 | stored, but stamped 7 |
| 5 | Read K2 | miss, because 7 differs from 8; source returns 'Hardware', stamped 8 |
| 6 | Read K1 and K3 | 2 misses, 2 source reads (Branch D, whole cache) |
Total source reads after the edit: 3 (K1, K2, K3), and no read returns 'Fasteners' for K2 after step 3, except the late fill that is rejected at step 5. Under per-key stamps the count would be 1, but only if the dependency of K2 alone is trustworthy.
Stop conditions
- Stop and fall back to an uncached lookup if the generation ever decreases, because that suggests a restore or a split source.
- Stop caching a key whose source read fails; never store the failure as a value.
- Stop relying on the design if edits can happen outside the transaction that bumps the number.
Proposed acceptance tests (to be run by you, none performed here)
- After step 3 in the fixture, no read of K2 returns the old value once the new generation is visible to that process.
- The late fill at step 4 never produces a hit.
- With the generation source made unreachable, Branch C behaves as the chosen policy says, and the logs show the policy name.
- Two processes: the second sees the new value within the written staleness window, measured and recorded.
- A forced generation decrease triggers the stop condition.
This is a design proposal. It does not claim any library or platform provides these behaviours, and the fixture numbers are hypothetical.