If state is the hard part of software, duplicated state is the sharpest edge of it. The moment the same fact exists in two places — a value and a cache of it, a database row and an in-memory copy, a count stored alongside the things it counts — you have quietly signed a contract to keep them equal forever. It’s an obligation with no natural end and no reminder. Nothing forces the two copies to stay in step; they agree only as long as every single code path that touches one remembers to touch the other, and code paths multiply while memory fades.

The trouble is that the two copies start out equal, which is exactly what makes the trap so patient. On the day you write it, the cache matches the source, the denormalized count matches the rows, everything agrees, and the design looks fine. The divergence comes later, from the path nobody updated: the one place that writes the row but forgets the cache, the batch job that deletes records without decrementing the counter, the error handler that rolls back one copy and not the other. Each is individually a small oversight. Collectively they guarantee that, given enough time, the two copies drift apart, and the system starts asserting two different values for something that is supposed to be one thing.

These are the bugs that feel impossible, and they feel that way because the logic really is correct — for the state it assumed. When the count says five and there are four rows, no single function is wrong; the code that reads the count is behaving perfectly given a count of five. The fault is that two things which were supposed to be one have separated, and no amount of staring at the read path reveals it, because the read path isn’t where the mistake lives. The mistake happened earlier, in whatever path let the copies diverge, and by the time you see the symptom that path has long since finished and gone.

This is the real content of the old joke that cache invalidation is one of the hardest problems in computing. It’s not hard because expiring an entry is technically difficult; it’s hard because a cache is a second copy of the truth, and keeping a second copy correct means correctly identifying every event that changes the first — including the ones you’ll add next year and the ones that happen in systems you don’t control. You are trying to enumerate a set that keeps growing. Miss one, and the cache serves a confident, fast, wrong answer, which is in some ways worse than a slow one, because nothing about it looks like failure.

The way out isn’t cleverer synchronization; it’s having fewer copies. Derive instead of store, so the second value is computed from the first on demand and cannot disagree with it. Keep one source of truth and make everything else a view over it rather than a sibling of it. When a real cache is genuinely necessary — and sometimes it is, for performance you can’t get otherwise — treat it as a deliberate, isolated exception with an explicit invalidation strategy, not as a casual convenience sprinkled around. Every duplicated fact is a synchronization obligation you’re promising to honor perfectly and forever, and the honest question to ask before creating one is whether you’d rather pay to keep two copies in sync or pay to compute the value each time. Surprisingly often, computing it is the cheaper bill.