A service caches session objects as native snapshots; after a deploy renames and moves those types, what breaks for the reader running the new code?
answer
- the code shape is the contract
- type identity resolves before anything else
- one bad type fails the graph
- rolling deploys read both directions
- stored bytes outlive the code
basics
~20 sThe snapshot names types by their old identity, so the new code cannot resolve them and the rebuild fails outright. The snapshot's contract is the code's shape, so an ordinary refactor is an unannounced breaking format change.
solid answer
~50 sRestoring a native snapshot starts by resolving the type named in each record. A renamed or moved type no longer resolves, so the reader running the new code fails on the whole graph, not on one field. Where the name still resolves, the **shape token** is checked next: if the type's field set changed, the reader typically refuses rather than guessing. A rolling deploy makes it two-directional at once — new instances read snapshots written by old ones (the backward direction), and old instances still running read snapshots written by new ones (the forward direction) — so both halves of the fleet can fail against the same shared cache. The underlying point is that these snapshots have no contract independent of the code: refactoring the types *is* the breaking change, and no review step sees it as one.
go deeper
Hold on to the core fact: a native snapshot names the exact types that wrote it, so renaming or moving a type makes bytes already stored unreadable.
Explain the reader's order of operations — resolve the type name, compare the shape token, then restore fields — and say what each of rename, add, remove and retype does to an already-stored snapshot.
Demonstrate the operational picture: a rolling deploy puts two builds on one cache so both compatibility directions are live at once, and a rollback leaves the whole fleet reading newer bytes than it understands. Then give the mitigations: shape identifier in the key, short entry lifetime, read failure treated as a miss.
Name the structural issue: persisting these bytes turns the code's shape into a durable contract with no artifact and no owner, so refactors become unreviewed breaking changes.
## The setting A service stores a session object as a native snapshot in a shared cache, and reads it back on the next request. It works perfectly, for months. Then a release renames two of the types involved and moves a third into another namespace. Nothing about that change looks like a format change: no schema file was touched, no interface was published, no consumer was notified. Yet at the moment the release rolls, cached bytes stop being readable. ## What the reader does, in order 1. **Resolve the type named in the record.** The payload identifies each object by a qualified type name. A renamed or relocated type does not resolve, and the failure is fatal for the whole graph — one unresolvable type deep inside the object means the root cannot be rebuilt at all. 2. **Compare the shape token.** When the name still resolves, the reader compares the token the writer stored — derived from the type's field shape — against its local declaration. A mismatch is normally refused rather than reconciled. 3. **Restore field by field.** Only if both checks pass are values written onto the instance, and even then a field the writer had and the reader no longer declares is discarded, while a field the reader declares and the writer never wrote is left empty. ## Walking an old snapshot with the new reader | Change made in the deploy | What the reader running the new code does with an old snapshot | |---|---| | Type renamed or moved | cannot resolve the name; the whole rebuild fails | | Field removed from the type | the stored value has nowhere to go and is discarded | | Field added to the type | nothing in the bytes for it; left at zero or empty | | Field's type changed | the stored value does not fit the declaration; the rebuild fails | | Field renamed | reads as one field removed and one added, so its value is silently lost | | A shape token pinned by hand | the reader attempts the rebuild despite the shape difference, and the two rows above apply | That last row is the interesting one: pinning the token by hand suppresses the mismatch check, which converts a loud failure into a quiet one. It is the right tool only when you have actually walked every changed field and decided what the reader should do with each. ## Two compatibility directions in one window A rolling deploy runs two builds at once against one cache, so both directions are live: - **Backward** — the new code reading bytes an older build wrote. This is the direction people anticipate, and it is the one that fires at rollout. - **Forward** — instances still on the old build reading bytes the new one has just written. This is the direction people forget, and during the rollout window it is guaranteed to happen if the cache is shared. A rollback makes the forward direction the *whole* fleet's problem: every instance is now old code facing a cache full of new snapshots. ## Why this is a storage problem, not a deploy problem The real defect is that a stored snapshot outlives the code that wrote it. The moment bytes persist across a release, the code's shape has become a durable contract, and the team that refactors a type is changing a contract they cannot see. Schema-driven and text encodings do not remove change, but they make the contract an artifact: it is named, it is reviewed, and its evolution rules say what a reader does with an unknown or missing field. If the family stays, the mitigations are about lifetime and detection rather than compatibility: 1. **Make the snapshot's lifetime shorter than the release interval** — an entry that cannot survive to meet new code cannot break against it. 2. **Put a build or shape identifier in the cache key**, so a new build simply misses instead of reading bytes it cannot interpret. 3. **Treat every read as fallible**, with a miss path that rebuilds the value from its source rather than an error path that returns a failure to the user. 4. **Refuse to hand-pin a shape token to make a mismatch go away** unless you have walked each changed field and know what the reader will do with it. The honest conclusion is the boring one: a value that has to survive a deploy wants an explicit encoding, and a native snapshot wants a lifetime measured in minutes.
- Which direction is backward compatibility here, and which is forward?Backward is the new code reading snapshots an older build wrote — that fires as the release rolls. Forward is an old instance reading snapshots the new build has already written, which happens during the rollout window and across the whole fleet after a rollback.
- What does renaming a single field do to a stored snapshot?The reader sees one field it no longer declares, whose stored value is discarded, and one field the writer never wrote, left empty. If a shape token stops the rebuild, this is loud; if the token was pinned by hand, the value disappears silently.
- If the family has to stay, what is the cheapest thing that makes this safe?Put a build or shape identifier in the cache key and treat a read failure as a miss. The new build then simply does not find old entries and rebuilds the value from source, which turns a version break into a cold cache.
- Why does a review not catch this change?Because there is no artifact to review. The format is implied by the type declarations, so the change arrives as a refactor in a pull request with no schema diff, no version bump and no consumer list to notify.
saying these in an interview costs you the question
- Thinks the reader matches types by field shape when a name changes.
- Assumes only the newly deployed code can hit a mismatch.
- Believes the type will not change, so the risk is theoretical.
- Pins the shape token by hand to silence a mismatch error.
- Thinks one unreadable field costs only that field's value.
- Treats a snapshot cache as durable storage.