When a meta-framework lets you replace its cache store with your own implementation, what must that implementation provide and guarantee?
answer
- a thin storage contract, not a policy
- get, set, remove by key or label
- entries leave the process as bytes
- an error on read is a miss
- latency budget on the render path
basics
~20 sA small storage contract: get an entry by key, set a whole entry with its metadata, remove entries. Bytes must be opaque and serialisable, keys come from the framework, and a failed read must degrade to a miss.
solid answer
~40 sThe seam is deliberately thin. The framework keeps the interesting decisions, what is cacheable, how the key is derived, when an entry counts as expired, and hands the store three jobs: `get(key)` returning an entry or nothing, `set(key, entry)` writing the payload together with its metadata, and a removal by key or label. Two guarantees matter more than the operations. First, an entry must survive leaving the process: it is serialised bytes plus metadata, written whole so no reader can observe half an entry. Second, the store sits on the render path, so it must be bounded in latency and fail soft, a read error is a miss and a write error is logged, never a failed response. Anything user-specific stays out of a store the whole fleet can read.
code
pseudocode · 13 linesinterface CacheStore:
get(key) -> Entry | MISS # Entry = { payload, meta }
set(key, Entry) -> void # written as one unit
delete(keyOrLabel)-> void
# the framework owns the key and the freshness rule; the store owns the bytes
handle(route, variant):
key = frameworkKey(route, variant)
entry = try store.get(key) catch -> MISS # an error is just a miss
if entry is MISS or frameworkExpired(entry.meta):
entry = render(route, variant)
try store.set(key, entry) catch -> log # never fails the response
return entry.payloadgo deeper
Know that the storage layer is swappable and that the framework, not the store, decides what is cached and under which key. Being able to name get, set and remove is enough at this stage.
Explain why entries must be serialisable and written whole, and why a read error should be treated as a miss. Show you understand which decisions stay on the framework side of the seam.
Talk about running it: a latency budget for the read, behaviour during a store outage, limiting duplicate renders across instances, and keeping anything user-specific out of shared storage.
Weigh the dependency itself. Decide which entries justify a fleet-wide store, what the failure budget is when it is down, and who owns its capacity, credentials and monitoring.
## Why the seam exists at all A meta-framework that caches rendered pages and fetched data has to put the bytes somewhere, and the right somewhere depends entirely on how the application is deployed: memory is right for one process, a store the fleet can reach is right for several, a managed cache is right on a hosting target that provides one. Rather than pick, frameworks commonly expose the storage layer as **a replaceable implementation** and keep the caching policy for themselves. Understanding which side of that line each responsibility falls on is the whole question. ## What stays with the framework - **What is cacheable at all**, and under which rendering mode. - **The key.** The framework derives it deterministically from the route, its parameters and whichever request variants it decided participate. A store that invented its own key would make an entry written by one instance unfindable by the next. - **Freshness policy.** How long an entry is good for, whether a stale entry may be served while a fresh one is produced, and what a label or tag attached to an entry means. - **What to do on a miss**: render, store, respond. ## What the store must provide A typical contract is three operations: 1. **`get(key)`** — return the stored entry, or nothing. The entry carries the payload and the metadata the framework wrote with it, unchanged. 2. **`set(key, entry)`** — write the payload and its metadata together, as one unit. 3. **`delete(key)` / `delete(label)`** — remove one entry, or every entry carrying a label the framework attached. That is usually the lot. The store is not asked to interpret payloads, to decide expiry, or to know anything about routes. ## The guarantees that actually bite | Guarantee | Why it matters | What breaks without it | |---|---|---| | Entries are opaque and serialisable | they must cross a process boundary | live objects or non-serialisable values fail on write, or corrupt on read | | Entries are written whole | readers and writers overlap constantly | a reader sees a payload without its metadata and treats a stale entry as fresh | | Keys are honoured verbatim | every instance must find the same entry | two instances quietly keep separate copies under one name | | Bounded latency | the read is on the render path | a slow store costs more than re-rendering the page | | Fail soft | the store is a dependency of every request | one store outage turns into an outage of the site | | Concurrency tolerated | several instances can miss the same entry at once | duplicated work, or a torn entry, or a write that resurrects deleted content | **Fail soft** is the one candidates most often miss. A read that throws should be treated exactly like a miss: render the page and answer normally. A write that throws should be logged and dropped; the response has already been produced, and failing it because the result could not be filed away converts a cache problem into a user-visible one. The same applies to a deletion that fails — it must be retried or surfaced, because silently swallowing it leaves stale content behind. ## The traps around the edges - **Size and eviction.** A store has finite room, and an entry can vanish before its lifetime is up. The framework must treat an early disappearance as an ordinary miss, so nothing may depend on an entry still being there. - **Last write wins.** Two instances rendering the same cold page will both write. That is normally fine because the payloads are equivalent, but it means the store must never merge partial writes, and it means a purge racing a slow in-flight write can be overwritten by it. - **Privacy.** Everything in the store is readable by every instance. A response assembled for one signed-in person must not be written there under a key that does not distinguish them. - **Payload growth.** What was a reference in memory is now bytes on the wire, every read. Caching a very large document may cost more to fetch than to rebuild. - **Ownership of the connection.** The store is infrastructure the application now depends on: credentials, network reachability from every instance, and health that someone monitors. ## How to talk about it Describe the contract in one breath — get, set, remove, opaque entries, framework-owned keys — and then spend your time on the operational half: latency budget, degrade-to-miss, whole-entry writes, nothing personalised. That ordering is what separates someone who has configured such a store from someone who has run one.
- Why should the framework compute the key rather than the store?Because the key is the agreement between instances. It has to fall out of the route, its parameters and the request variants the framework decided matter, identically in every process. A store that derived its own key from anything local, such as a hostname or a connection, would give each instance a private entry under what looks like one name.
- What should happen when the shared store is unreachable for a minute?Every read behaves as a miss and every write is dropped with a log line, so pages are rendered on demand and users see correct if slower responses. The risk is load: the whole fleet renders everything at once, so a sensible implementation adds a short in-process fallback and a limit on concurrent identical renders.
- Is it safe to cache a response that varies by signed-in user in a shared store?Only if the key distinguishes the user and the store is trusted with that content, which is rarely worth it for rendered pages. The default answer is no: a fleet-wide store is the wrong place for per-person output, because one wrong key or one missing variant hands one person's page to another.
saying these in an interview costs you the question
- Letting the store invent its own cache keys
- Turning a store read error into a failed response
- Writing payload and metadata as separate operations
- Assuming an entry stays until its lifetime expires
- Putting per-user rendered output in a fleet-wide store
- Ignoring that every read now costs a network round trip