What must a cache tier shared across requests never hold, and what must its key carry?
answer
- audience, not storage, is the difference
- the key is an access-control decision
- an always-on predicate skips cache hits
- narrowing dimensions belong in the key
basics
~20 sKeep out anything shaped per actor, anything unbounded, and anything a transaction will read then write a decision on. The key must carry the type, the identifier, every dimension the read was narrowed by including tenant, and a shape marker.
solid answer
~50 sA shared tier changes the audience of a mistake: an entry is served to every request on the process, or on every node when the store is shared. So the key is an access-control decision, not a naming convention. Most layers add an always-on predicate — tenant, not-deleted, effective date — while **building a statement**; a tier hit builds no statement, so that narrowing has to live in the key or it is gone, and an entry keyed by identifier alone can be handed to another tenant. Beyond the key, keep out anything shaped per actor, any collection whose size grows with the data, and anything you cannot name the writes for. And at no tier cache a value a transaction reads and then writes a decision on: that needs a locking read or a conditional `UPDATE ... WHERE`.
go deeper
Take away one rule: an entry in a cache shared between requests can be handed to a different user than the one it was loaded for, unless the key says who it belongs to.
Explain why narrowing has to be in the key: the predicate that would have restricted the rows is applied when a statement is built, and a hit never builds one.
Show an admission process rather than instinct: who may see the row, which writes make it wrong, how large one entry can grow, and what one stale hit actually costs.
Own the policy. Decide which types may ever be admitted, who reviews a new key, and where the system deliberately accepts staleness against where it must not.
## Why a shared tier is a different kind of risk A copy held inside one unit of work is a private mistake: it is visible to one request and it disappears when that request ends. A copy held in a tier shared across units of work is a public one. Every request on the process — every node, when the store behind the tier is shared — can be served that entry, for as long as it survives eviction and expiry. That change of audience, not the storage mechanism, is what decides which values may be admitted. ## The key is an access-control decision Most layers can attach an always-on predicate to the statements they build: a tenant column, a not-deleted flag, an effective date. The predicate is applied **when a statement is built**. A tier hit builds no statement, so the predicate does not run. Whatever narrowing that predicate was providing has to be carried in the cache key instead, or it is simply gone. That gives the rule for keys in any shared tier: 1. **The type and identifier**, so the entry is addressable at all. 2. **Every dimension the read is narrowed by** — the tenant, the partition, the effective date, any always-on predicate's inputs — because none of them will be re-applied on a hit. 3. **A shape marker**, so a change to what the entry contains (extra columns, a different projection) cannot be answered from an entry built by the previous version of the code. 4. **Nothing else that varies per request.** A key that includes a request identifier or a timestamp is a key that never hits. The failure this prevents is not subtle. An entry keyed by identifier alone, loaded by one tenant's request and served to another's, is a cross-tenant data disclosure caused by a performance feature. ## What must not go into a shared tier | Candidate | Verdict | Why | |---|---|---| | A row read by identifier, rarely written | admit | the tier's core case | | A row narrowed by an always-on predicate | admit only if that predicate's inputs are in the key | the predicate does not run on a hit | | Anything shaped per actor — a personalised list, a permission-filtered set | keep out | the audience is wrong by construction | | A collection whose size grows with the data | keep out | one entry can hold an unbounded amount of memory | | Data with a retention or residency obligation | keep out unless the tier honours it | an eviction policy is not a deletion guarantee | ## What stays out of every tier, private one included - **A value a transaction will read and then write a decision on.** A balance check followed by a debit needs a read the database serialises — a locking read such as `SELECT ... FOR UPDATE`, or a conditional `UPDATE ... WHERE` that fails when the row moved. A cached value cannot carry that guarantee, at any tier. - **A value whose correctness window is shorter than the shortest lifetime you can configure.** If nothing may be more than a moment old, caching buys nothing and hides the failure. - **Anything you cannot invalidate.** If no code path can be pointed at the entry when the underlying row changes, admitting it is a choice to serve stale data indefinitely, not a caching strategy. - **Secrets and credentials in a form that survives the request.** A tier is a store; treat it as one. ## Auditing a candidate before you admit it Before adding a type or a query to a shared tier, answer four questions in order: 1. **Who may see this row?** If the honest answer is "it depends on who asked", either the dependency goes in the key or the value stays out. 2. **What makes it wrong?** Name the writes. If you cannot name them, you cannot invalidate them. 3. **How big can one entry get?** Bounded, or not admitted. 4. **How bad is one stale hit?** A stale display name is a nuisance; a stale entitlement is an incident. ## The honest caveats Layers differ in how much of this they enforce. Some build tenant narrowing into the key automatically once the predicate is declared to the layer; some leave the key entirely to you; some offer per-type admission rules and some are all-or-nothing. Never assume the safe behaviour is the default, and never assume a short expiry converts a correctness bug into a performance trade-off — a wrong entry served for five seconds is still a wrong entry served, and under load five seconds is a great many requests.
- Why is a short expiry not an answer to a tenant-keyed entry being served to the wrong tenant?Because it changes the duration of a disclosure, not its nature. Under load, seconds are thousands of requests, and the entries most worth caching are exactly the hot ones being read constantly. Expiry bounds staleness; only the key bounds who may be served.
- What is wrong with caching a collection under a shared tier entry?Its size is set by the data, not by you, so a single entry can grow without bound and one write anywhere in it invalidates the whole thing. Cache the identifiers of a bounded, deliberately-limited result if you must, and let each element be resolved on its own.
saying these in an interview costs you the question
- Thinks a globally unique identifier makes a tenant key unnecessary
- Assumes an always-on predicate is re-applied on a cache hit
- Caches a collection whose size grows with the data
- Caches the value a transaction will decide and then write on
- Believes a short enough expiry makes cross-tenant leakage acceptable
- Cannot name the writes that would make an entry wrong