intermediate 2 min answer

A single-page application shows stale data after a user makes a change elsewhere in the product. How should client-side caching and invalidation be designed?

client-cacheinvalidationoptimistic-updatesstalenessux
Show the full answer Hide the answer

Why this happens

The client caches server responses to avoid refetching. The cache has no way to know that a mutation elsewhere in the application invalidated an entry, so a user who updates a record on one screen returns to a list showing the old value — and concludes the change did not save.

The perception cost exceeds the technical cost. Users respond to apparent data loss by repeating the action, which produces duplicates.

The design

  • A normalised cache keyed by entity identity, so an entity appearing in a list and a detail view is one cached object. Updating it once updates every view — which eliminates the largest class of this problem structurally.
  • Explicit invalidation on mutation: a mutation declares which queries it affects, and those are refetched or marked stale. This is the mechanism, and the declaration must be maintained — a mutation that forgets to declare an affected query is the recurring bug.
  • Stale-while-revalidate as the default read behaviour: show the cached value immediately, refetch in the background, update when it arrives. The user sees something instantly and correctness follows within a moment.
  • Optimistic updates for the acting user, with a rollback path and honest notification on failure — routinely under-built, because rejections are rare in testing and confusing in production.
  • Refetch on window focus and on reconnection, which catches changes made in another tab or on another device cheaply.
  • A defined staleness policy per data type, since a user's own profile, a shared document and a reference list have completely different tolerances — applying one policy everywhere is either wasteful or wrong.

For genuinely collaborative data

Where multiple users change the same data concurrently, invalidation is insufficient because the client does not know a change occurred.

  • Server-pushed invalidation or updates over a websocket or server-sent events, so the client learns rather than polls.
  • Polling with a sensible interval where a push channel is not justified.
  • Versioned entities, so a stale write can be rejected rather than silently overwriting.
  • And an explicit decision about conflicts: last-writer-wins per field handles most cases well, while long-form text needs the conflict surfaced to the user, because silent resolution destroys work.

The framing that matters

A client cache is a replica, and every replica has a consistency model — whether or not anybody chose one. Most staleness bugs are the result of a consistency model that was never stated, so the useful discipline is naming it per data type: this is read-your-writes, this is eventually consistent within a minute, this is refreshed on focus.

And where staleness is unavoidable, saying so in the interface converts a perceived bug into communicated behaviour — which is the cheapest fix available and the one most often skipped.