How would you set an app-wide policy for which cached copies may hold a personalised read?
answer
- default deny for identity-derived reads
- classify by audience before placing
- scope and lifetime outlive tier names
- split the page, do not ban the cache
- two identities in continuous integration
basics
~20 sDefault-deny: anything derived from who is asking stays request-bound or in the browser. Classify reads by audience, make placement visible at the call site, split pages so personal holes sit outside shared copies, and enforce it with a two-identity test.
solid answer
~50 sWrite the policy in terms of scope and lifetime rather than a framework's tier names, because those names differ by product and change between versions while the axes do not. Start from default-deny: a read derived from identity may live in a request-scoped copy or in that browser, and anywhere longer-lived only by exception with a complete key and a named owner. Classify reads into public, per-segment, and per-identity, and make that classification visible where the read is written, not buried in configuration. Prefer splitting a page - shared shell, personalised holes - over banning caching, so the policy does not read as "cache nothing" and quietly get ignored. Enforce it mechanically with a two-identity test on representative routes in a production-shaped build, and treat a scope violation as a security incident with a different bar from a staleness bug.
go deeper
The takeaway is the default: if a value depends on who is signed in, do not put it anywhere that outlives the request. Ask before making an exception.
Be able to classify a read as public, per-segment, or per-identity and justify the placement that follows from that class.
Show how the rule is enforced rather than merely stated: a two-identity test on real routes, a review checkpoint where a read is marked cacheable, and containment that does not wait on a deploy.
Own the asymmetry and the durability - a scope error is a disclosure while a lifetime error is staleness, and the policy must be written on axes that survive a framework upgrade.
## Why this needs a policy at all Placement decisions are made one read at a time, by whoever is writing that read, usually while thinking about latency rather than audience. The failure they produce is not proportional: a wrong lifetime shows someone an out-of-date number, while a wrong scope shows one customer another customer's data. A per-read judgement call with asymmetric consequences and no default is exactly the situation a written policy exists for. The second reason is turnover in the tooling. Frameworks differ in how many places a copy may live and in what they do by default - some cache aggressively unless told otherwise, others cache nothing on the server and leave it to HTTP and the host - and those defaults change across major versions. A policy phrased in one product's tier names is obsolete on the next upgrade. A policy phrased as **who may be served this copy, and for how long** survives it. ## Classify reads before you place them Three classes are usually enough: | Class | Example | May live in | | --- | --- | --- | | Public | catalogue, article, marketing page, pricing table | any copy, including shared and long-lived ones | | Per-segment | locale, currency, country, plan tier | a shared copy whose key contains every segment input | | Per-identity | account, cart, entitlements, anything from a session | request-scoped, or the browser's own copy | The rule that follows is short enough to remember: **a read derived from who is asking does not go in a copy that outlives the request.** Exceptions exist - an expensive per-user dashboard reused many times in a short window - but they should be exceptions with a complete key, an owner, and a review, not a pattern anyone can reach for. ## Make placement visible where the read is written A policy nobody can apply at the moment of writing is decoration. Practical measures: - Keep the identity-reading primitives in a small number of modules, so "this read touches the session" is greppable. - Require that a read marked cacheable declares its class, so the reviewer sees audience and latency in the same diff. - Make the safe option the easy one: the default helper should be the request-bound one, with the shared placement the thing you opt into by name. - Avoid configuration that changes placement from far away - a route-level setting in another file is how a read that was request-bound on Monday becomes shared on Friday without its author knowing. ## Do not let the policy collapse into "cache nothing" The most common failure of a strict policy is that it is too blunt to live with, so teams quietly route around it. The defence is structural: most pages are a large shared surface with a few personal holes. Navigation, layout, catalogue, and article bodies are the same for everyone; the name in the corner, the cart count, and an entitlement banner are not. Splitting on that line keeps the expensive shared part reusable while the personal part is produced per request or filled in from the browser. Frameworks vary in how directly they support the split - some let part of a response be reused while another part is produced per request, others make caching an all-or-nothing property of a route - and the policy should say what to do in both cases rather than assume the convenient one. ## Enforce it mechanically 1. **A two-identity test** on the routes that matter: warm as A, request as B in a clean context against a production-shaped build, and assert B never sees A's data. This is the only test that catches a scope failure, and a single-identity suite will pass forever without it. 2. **A review checklist** at the point a read is marked cacheable: what varies the output, is all of it in the key, who owns the exception. 3. **A build or deploy signal** when the set of routes that are served from a shared copy changes, so the change is noticed by someone other than its author. 4. **An incident class** for scope violations, separate from staleness, with its own severity and its own postmortem question: which other reads were placed by the same reasoning? ## Account for deploys and rollback Copies do not all end at the same moment. Some are thrown away by a new build; others live in storage or in front of the origin and survive it, which means a bad copy can outlast the deploy that fixed the code. The policy should therefore say how a copy is removed, who can do it, and how long it takes - because "we shipped the fix" is not the same statement as "nobody can be served the exposed bytes any more." The team that learns this during an incident learns it expensively. ## What good looks like A new engineer can read one page, classify the read they are about to write, pick the placement without asking, and have the reviewer check it in seconds. The policy names no product, survives an upgrade, and makes the safe choice the default - and the one test that could catch a violation runs on every change.
- How do you stop a strict policy turning into "cache nothing"?Give teams the split instead of a ban: keep the shared shell reusable and move the personal holes to a request-bound part or to something the browser fills in. Most of the latency win lives in the shared surface, so the policy stays affordable, and engineers stop looking for ways around it.
- Why should the policy avoid naming a framework's cache tiers?Tier names, their defaults, and even how many exist differ between frameworks and shift across major versions, so a policy written in them ages into something that reads as wrong. Scope and lifetime are properties of every implementation, so a rule phrased in those two axes stays applicable through upgrades and through a change of framework.
- What makes a scope violation a different class of incident from stale data?Staleness shows a user an out-of-date version of something they were already entitled to see. A scope violation shows them another person's data, which is a disclosure with legal and trust consequences, may require notification, and cannot be undone by fixing the code. It deserves its own severity, its own containment step, and a postmortem that sweeps for the same placement decision elsewhere.
saying these in an interview costs you the question
- Writes the policy in one framework's tier names
- Answers a leak by banning caching across the whole application
- Leaves placement invisible at the call site
- Treats a cross-user leak as equivalent to serving stale data
- Assumes a deploy removes every copy that could be exposed
- Has no test that uses two identities against one URL