After freezing a mutable buffer into an immutable cart, what goes wrong if a reference to that buffer survives?
answer
- two paths to the same storage
- the handle outlived its build
- cached total stops matching the lines
- copy on freeze, or retire the handle
- never freeze a buffer you were given
basics
~20 sIf the freeze handed over the buffer's storage instead of copying it, a surviving handle can still write that storage — so a cart callers were told could never change does change, and every conclusion drawn from its immutability becomes unsound.
solid answer
~40 sA freeze that wraps the buffer's storage without copying leaves two paths to the same slots: the published cart and the buffer handle. A later write through the handle edits a value other code believes is fixed, and the damage is not confined to that read — totals computed once at construction now disagree with the lines, and anything that cached the cart on the strength of its immutability is serving something untrue. The symptom appears far from the guilty write and is hard to reproduce. Two disciplines make it impossible: copy the contents at freeze so the value owns its storage, or hand the storage over and retire the buffer handle so any later write fails immediately.
code
pseudocode · 10 linesfunction buildCart(lines)
buffer = newMutableList()
for each line in lines
buffer.add(line)
cart = freezeWrapping(buffer) // no copy: cart reads buffer's storage
return { cart: cart, buffer: buffer } // the leak
result = buildCart(lines)
result.buffer.add(extraLine)
// result.cart now lists extraLine, and its total predates itgo deeper
Remember the rule of thumb: once you have frozen a buffer into an immutable value, stop using the buffer. Let it go out of scope inside the function that created it.
Explain the mechanism — a freeze that hands over storage without copying leaves two paths to the same slots, and a write through the surviving one edits a value callers believe is fixed.
Describe the symptom and the fix: totals that disagree with the line items, reported far from the guilty write, fixed by copying at the freeze or retiring the handle so a later write fails loudly.
Judge which discipline the codebase should standardise on and what makes it enforceable — an ownership convention plus a freeze that retires the handle, rather than a line in a review checklist.
## Two honest ways to freeze Turning a mutable buffer into an immutable value can be done in two sound ways, and they trade cost against ceremony. | | copy on freeze | hand-off with invalidation | |---|---|---| | what the value's storage is | fresh storage the value alone owns | the buffer's own storage, transferred | | cost of the freeze | Θ(m) — one full copy | Θ(1) | | the buffer handle afterwards | still usable, and harmless | retired; a later write fails at once | | when it is the right choice | the buffer came from somewhere you do not control | you created the buffer and own it outright | The unsound third option is the one that ships: hand the storage over **and** leave the handle usable. It has the cost profile of the second and the safety of neither. ## What a surviving handle actually breaks A write through the stale handle does not merely change some data. It falsifies everything the rest of the program concluded from the value being fixed: - **Derived data computed once.** A cart computes its totals when it is built. A line appended afterwards through the stale handle is in the collection but not in the total, so the cart now reports a sum that does not match its own items. - **Snapshots handed out for reading.** Code that accepted the cart precisely because it could not change may have passed it on, stored it, or compared it against a later version. - **Caches keyed on the value.** An entry cached under the cart's contents no longer describes the contents. - **The reasoning itself.** "I do not need to re-read this, it cannot have changed" is the payoff of immutability, and it is exactly the inference the leak invalidates. ## Why the symptom shows up far from the cause The write that causes the damage is legal, local and successful — a line added to a buffer the builder still holds. Nothing fails there. The failure surfaces later, in a reader that never touched the buffer, as a total that disagrees with the line items or a comparison that says two snapshots are equal when they were taken minutes apart. That distance is what makes the bug expensive: - it reproduces only when the builder is reused, which may depend on load or on a particular call order; - the stack at the point of the symptom contains nothing connected to the write; - the value's own declaration says it is immutable, so the investigation starts by trusting the wrong thing and looks everywhere else first; - the fix, once found, is one line in code that looked innocent, which makes it easy to dismiss as a one-off instead of a class. ## Rules that keep it out of the codebase 1. **Give the buffer the shortest possible life.** Create it inside the function that fills it, freeze it there, and return only the frozen value — never the pair. 2. **Never freeze a buffer you were handed.** If the buffer arrived from a caller, you cannot know what else holds it, so copy on freeze and pay the Θ(m) once. 3. **Do not keep a buffer in a field for reuse.** A reused buffer is a stale handle by construction, and the second build corrupts the first build's result. 4. **Prefer a freeze that retires the handle** where the language or the type lets you express it, so a later write fails at the guilty line instead of silently succeeding. ## Reviewing for it The review question is not "is this type immutable?" but **"can anything still write the storage this value reads?"**. That turns into a short search: does any buffer outlive the function that froze it — as a returned value, a field, a variable captured by something longer-lived, or a parameter that came in from outside? If a buffer's lifetime is wider than the build that used it, either the freeze copies or the design is wrong. The same question applies to the source of the buffer: filling a buffer that is actually the published cart's storage is the mirror-image mistake, and it corrupts the value before the new one is even built.
- Which freeze discipline would you pick on a hot path?Hand-off with invalidation, when you created the buffer yourself: it avoids the Θ(m) copy, and retiring the handle makes a later write fail at the line that wrote it. Copy on freeze is the right answer whenever the buffer came from a caller, because you cannot know what else still holds it.
- How do you catch this in review rather than in production?Look for a buffer whose lifetime is wider than the build that filled it: one that is returned, stored in a field, captured by something longer-lived, or passed in from outside. If a buffer outlives the function that froze it, the freeze must copy.
- What is the mirror-image mistake at the start of the build?Filling a buffer that is the already-published cart's own storage instead of a copy of it. The edits then land in a value other code is holding, corrupting it before the new cart is even produced — the same aliasing fault, at the other end of the build.
saying these in an interview costs you the question
- Calls a value immutable because its type says so, whatever storage it shares.
- Freezes a buffer handed in by a caller without copying it.
- Keeps the buffer in a field and reuses it for the next build.
- Expects the failure to surface at the line that wrote through the handle.
- Trusts a cached total on a value whose storage another handle can still write.