skip to content

After a write to a single item resource in your API, which other cached representations become stale, and how would you design invalidation so that clients do not read a stale collection, summary or count?

level: principalimportance: should knowfreq 36%

answer

  1. Item + listings + pages + counts + parents
  2. Create/delete shifts membership across pages
  3. Read path declares deps; write path purges once
  4. Coarse vs fine keys = safety vs hit ratio
  5. TTL backstop + soft purge + measure staleness

basics

~20 s

A write invalidates far more than the item: every listing, filtered or paginated page, search result, count, aggregate and parent resource that embedded it. Model the dependency explicitly — tag responses with the entities and collections they read — and purge the whole dependency set from one shared write path, with TTLs as the backstop.

solid answer

~60 s

The stale set after one write is almost always larger than teams expect: the item itself, every collection page and filter variant that included it, search results, counts and aggregates, parent or embedding resources, and — for a create or delete — pages whose *membership* shifted, which can cascade across pagination. The only strategy that survives growth is to make the dependency explicit rather than remembered. Have the read path record which entities and collections each response consulted, and emit those as cache tags. Have one shared write path map "entity X changed" to the set of tags to purge, so every handler that mutates X gets identical invalidation for free. Do it after commit, driven from an outbox or change stream so a crash cannot lose the purge. Then accept that you cannot make it perfect and bound the failure: keep a finite `s-maxage` as a backstop, prefer soft purge so a write does not stampede the origin, and be explicit in the API contract about which endpoints are strongly fresh and which are eventually fresh.

go deeper

for a junior

Recognise that a write makes more than the item stale — listings and counts that included it are stale too — and that something must clear them.

for a middle

Enumerate the dependency set including pagination and aggregates, and describe tagging responses with the entities and collections they read so one purge covers them.

for a senior

Own the mechanics: dependency capture in the data layer, a single write-path purge mapping, durable purges via an outbox, purge-after-commit ordering, and a TTL backstop.

for a principal

Frame it as a consistency contract — classify endpoints as strongly fresh, eventually fresh or best effort, trade key granularity against write rate, state the read-your-writes guarantee, and measure observed staleness rather than assuming correctness.

## The stale set is a graph, not a row A write updates one record; it invalidates a *neighbourhood* of representations. Enumerate them honestly: - **The item representation** — obvious, and the only one most teams handle. - **Collections containing it** — and not one collection but every page, every sort order, every filter combination, every page size, every content-negotiated variant. - **Membership shifts** — on a create or delete, the item moves in or out of the set, which shifts every subsequent page in a paginated listing. This is the case URL-based thinking always misses, because there is no item URL to purge for something that did not exist yet. - **Derived data** — counts, totals, facet buckets, leaderboards, dashboards, feeds. - **Embedding and parent resources** — an order that embeds a line item, a user resource that inlines a preference, a search index projection. - **Cross-entity effects** — renaming a category changes representations of every product in it. Any approach that asks a developer to remember this list at each write site fails, because the list changes whenever someone adds an endpoint. ## Make dependency a property of the read path The robust design inverts responsibility: **the response declares what it depends on**, at the moment it is built. Instrument the data-access or serialization layer so that loading entity 42 records a dependency on `product-42`, and querying the product collection records a dependency on the coarse `products` (or `products:category-shoes`) key. At response time, the accumulated set is emitted as cache tags on the response. This has the property you need: a new endpoint added next quarter that happens to read products is tagged correctly without anybody thinking about it. Dependency tracking follows the code that actually reads data, so it cannot drift from reality the way a hand-curated list does. ## Make purging a property of the write path — once Symmetrically, define one mapping from a domain change to the tags it invalidates, and route every mutation through it. Creating, updating or deleting a product purges `product-42` **and** the collection keys for the sets it belongs to. Because collection membership changes are covered by the coarse key, pagination cascades are handled without enumerating pages. Make the purge durable. A purge issued inline can be lost if the process dies after commit; drive it from a transactional outbox or a change-data-capture stream so "committed" and "invalidated" cannot diverge for long. Purges are idempotent, so at-least-once delivery is exactly right. And issue it after the write is visible to readers — purging before commit lets a concurrent read re-cache the old value, which is worse than not purging at all. ## Granularity is a real tradeoff Coarse keys are safe and cheap to reason about but destroy hit ratio: tagging every listing response with a single `products` key means any write to any product flushes all listings. Fine keys preserve hit ratio but multiply cardinality and risk gaps. The usual resolution is intermediate keys aligned with how the data is actually queried — per category, per tenant, per region — so a write flushes a meaningful slice rather than everything or nothing. This is a judgement call driven by write rate: at low write rates coarse keys are fine; at high write rates coarse keys mean the cache is permanently cold. ## Decide what does not need to be exact Not every derived representation deserves synchronous invalidation. Counts, trending lists, dashboards and analytics aggregates are frequently better served by a short TTL and an honest statement that they lag. Trying to keep every aggregate perfectly consistent with every write converts a caching layer into a distributed-consistency project. The principal-level move is to classify endpoints explicitly: **strongly fresh** (purged on write, staleness measured in milliseconds), **eventually fresh** (short TTL, staleness measured in seconds), and **best effort** (long TTL, refreshed on a schedule). Publish that classification so consumers build correctly against it. ## Read-your-writes Even perfect invalidation leaves a user-visible race: a client that writes and immediately reads may hit a cache node that has not yet been purged, or an origin read replica that has not yet caught up. Common remedies are to have the write response return the new representation directly so the client need not re-read, to route the immediate follow-up read past the cache, or to have the client hold its own optimistic value briefly. Deciding which of these the API guarantees is part of the contract, not an implementation detail. ## Bounding failure Finally, assume invalidation will sometimes fail: a purge is dropped, a dependency was never tagged, a new endpoint slipped through. Keep a finite shared-cache TTL so any miss self-heals within a known window. Prefer soft purge so a burst of writes does not translate into a burst of origin misses. Instrument observed staleness — timestamp inside the payload versus wall clock at the edge — so you can measure whether the design works instead of assuming it. The goal is not zero staleness; it is staleness that is bounded, measured and documented.

  • Why does creating a new item invalidate more than updating an existing one?
    An update changes the content of a representation that already carries that item's tag, so the item's own key reaches it. A create changes set membership: the new item appears in listings, shifts every subsequent page of a paginated collection, and alters counts and facets — yet no cached page carries a tag for an entity that did not exist when they were built. Only a coarse collection key attached to every membership-dependent response covers that case.
  • How do you decide between one coarse cache key per collection and fine-grained keys?
    By write rate against hit ratio. A single coarse key is simple and gap-free but means any write flushes every cached listing, so at high write rates the cache stays cold and the strategy defeats itself. Fine keys preserve hits but multiply index cardinality and create room for gaps. The practical middle ground is keys aligned with real query dimensions — tenant, category, region — so one write flushes a meaningful slice.
  • A user updates a record and immediately re-reads it, but sees the old value. Invalidation is working correctly. What is happening and what do you do?
    The purge is asynchronous relative to the client's next request, or the origin served the refresh from a replica that had not caught up, so the cache was re-populated with pre-write data. Remedies are to return the updated representation in the write response so no re-read is needed, to route the immediate follow-up read past the cache, or to guarantee the refresh path reads from the primary. Which of these the API promises should be stated in the contract rather than left to chance.

Correcting one entry in an encyclopedia also invalidates the index, the contents page and every cross-reference — reprinting only the entry leaves readers navigating by stale pointers.

saying these in an interview costs you the question

  • Invalidating only the item's own URL after a write and assuming listings will catch up.
  • Maintaining the list of affected representations by hand in each write handler.
  • Ignoring that a create or delete shifts membership across every page of a paginated collection.
  • Issuing the purge before the write is committed and visible, allowing the old value to be re-cached.
  • Insisting every aggregate and count must be perfectly consistent with every write, instead of classifying endpoints by required freshness.

context