Adding a cache is usually framed as a pure performance decision — this is slow, caching makes it fast, done. That framing skips the part that actually matters: a cache is a bet that a value computed earlier is still true now, and every cache carries an implicit promise to the code that reads it — this is current — that the caching layer has to actually keep. The speed is real and often worth it. But the promise is the part that creates risk, and it’s easy to add the cache for the speed while forgetting you also just took on the promise.

The trouble is that the promise is invisible in the code that benefits from it. Code reading from a cache looks identical whether the cache is perfectly fresh or badly stale — same function call, same return type, same apparent confidence. There’s no syntax for “this value might be from ten minutes ago,” so the caller has no local signal that they’re trading correctness for speed, and neither does anyone reading the code later. The tradeoff was made once, at the caching layer, and then it disappears from view everywhere the cached value gets used — which is exactly the shape of an assumption quietly baked into the system, the same pattern the scale thread described, just applied to time instead of size.

This is why cache invalidation earns its reputation as one of the genuinely hard problems: it’s not hard to write cache-miss-then-populate logic, that part is mechanical. It’s hard because invalidation requires knowing every path by which the underlying value can change, and missing even one path means the cache can silently diverge from reality with no error, no crash, just a value that’s wrong in a way nothing detects. A record updated through a different code path, a direct database change, a second service writing to the same data — each is a way for the promise to quietly break while the cache keeps confidently serving the old answer, and the caller has no way to tell the difference between “fresh” and “stale but nobody noticed yet.”

The costs of getting this wrong aren’t symmetric, which is worth being deliberate about rather than defaulting into. Serving stale data too long produces a correctness bug — wrong information presented with the same confidence as right information, often the more dangerous failure because nothing about it looks like a failure. Invalidating too eagerly just gives back some of the performance you cached for, a much cheaper mistake to make. When the invalidation logic is uncertain, that asymmetry argues for erring toward invalidating rather than serving something you’re not sure is still true, because a slower correct answer beats a fast wrong one in almost every domain that matters, and the two failure modes are not equally forgivable.

None of this argues against caching — the performance case is frequently real and the tool is frequently right. It argues for treating the invalidation strategy as the actual design decision, with the caching mechanism as the easy part built on top of it. Before adding a cache, the question worth answering explicitly is: every way this value can change, how does the cache find out? If there’s a clean answer, cache with confidence. If the answer is “mostly, probably,” that gap is exactly where a future incident is already scheduled — invisible today, for the same reason every promise in this thread has been invisible: nothing about the code reading a cached value tells you it’s making a bet at all.