skip to content

The routing table and its matching weight map are immutable but sit behind separate references; what can a reader observe?

level: seniorimportance: nice to knowfreq 30%

answer

  1. atomicity follows the reference
  2. two references, two handovers
  3. each half whole, pair mismatched
  4. ordering picks the mismatch, not removes it
  5. join them into one composite value

basics

~20 s

A pair that was never published together: the new table with the previous weights, or the reverse. Each value is whole, but atomicity follows the reference, so two references mean two independent handovers and a window between them.

solid answer

~40 s

Indivisibility is a property of the handover, not of immutability. Each reference changes in one step, so a reader sees a complete table and complete weights, but a reader that reads the two references at two moments can land between the publisher's two handovers and pair a new table with the weights that preceded it. Publishing them in a fixed order only decides which mismatched pair is possible; it does not remove the window, and nor does shrinking the interval. If an invariant genuinely spans the two, they are one value: build a composite holding both, hand over that composite once, and a reader gets both or neither.

code

pseudocode · 13 lines
pseudocode
// two references: a reader can land between the two handovers
function publishSeparately(newTable, newWeights)
    routes  = newTable      // a reader reading `routes` here...
    weights = newWeights    // ...and `weights` after this, holds a mismatched pair

// one reference: the pair changes in a single step
function publishTogether(newTable, newWeights)
    fresh = { table: newTable, weights: newWeights }   // built where nobody can reach it
    plan  = fresh                                      // one handover: both or neither

function handle(request)
    current = plan                                     // one read, matched halves
    return route(request, current.table, current.weights)

go deeper

for a junior

Take away the boundary: one handover covers one reference. Two values behind two references can be read at two different moments, even when neither value ever changes.

for a middle

Walk the interleaving out loud, then propose the fix: if a single decision reads both values, put them in one holder and hand that over once so a reader gets both or neither.

for a senior

Recognise the production signature, a rare load-dependent decision that should be impossible, and resist the narrowing fixes: closer handovers and fixed ordering both leave the window open.

for a principal

Decide where consistency units are drawn across the system and say so explicitly, weighing an occasional mismatched pair against coupling the release cadence of two values that different teams publish.

## What the handover actually makes indivisible One handover makes one reference's change indivisible. That is the entire guarantee. It is easy to hear the broader claim instead, that publishing an immutable value atomically replaces *the state*, but the unit is the reference, and a design with two references has two units. So with a routing table behind one reference and its weight map behind another, a reader performing two reads can be interleaved with a publisher performing two handovers: 1. The reader reads the table reference and gets version `v1`. 2. The publisher hands over the table reference to `v2` and the weight reference to `w2`. 3. The reader reads the weight reference and gets `w2`. The reader now holds the pair `(v1, w2)`, which no publisher ever intended and which no consistent state of the system ever contained. Both halves are internally coherent; the pair is not. Nothing was corrupted and nothing was torn, so every structural argument about immutability still holds, and the answer is still wrong. ## Why the obvious fixes do not work - **Ordering the two handovers.** Publishing weights first and then the table only changes which mismatched pair a reader can end up with. The window between two independent changes exists in either order. - **Shrinking the interval.** Making the two handovers adjacent narrows the window and turns a reproducible defect into a load-dependent one. That is a worse outcome, not a fix. - **Deepening the immutability of each half.** No amount of guarantee about what is *inside* either value says anything about the relationship *between* them. - **Re-reading one of the references.** Reading again gives a newer value, not a matched one; the reader still has no way to know which version its other half came from. ## The fix: make the unit match the invariant If the invariant spans both values, they are one value: | | Two references | One composite reference | |---|---|---| | Handovers per publication | two | one | | What a reader can observe | any pairing across the window | only pairs a publisher built | | Cost of a publication | rebuild only the half that changed | rebuild the composite wrapper | | Where the invariant lives | nowhere, implicitly | in the type that holds both | Build a small immutable holder containing both halves, assemble it off to the side, and hand over that one reference. The wrapper itself is cheap, because it can point at an unchanged half rather than duplicating it; what you are paying for is a second allocation per publication, and what you buy is that a mismatched pair becomes unrepresentable rather than merely unlikely. ## When separate references are still right Not every pair of shared values needs joining: - when no invariant relates them, so any pairing is legitimate; - when they change at very different rates and a joint publication would force needless rebuilds of the stable half; - when different publishers own them and coupling their publication would couple those owners. The test to apply is not how related the two values feel, but whether any single decision in the process reads both. If a decision reads both, the two are one consistency unit and should be behind one reference. ## How this shows up in production The signature is a decision that *cannot happen* appearing rarely in the logs: a weight applied to a route that the table no longer contains, a default taken because a lookup on the fresher half missed. It is load-dependent, it disappears in a debugger, and it is often misfiled as a visibility problem in the shared references. It is not; each reference behaved exactly as specified. The design simply split one consistency unit across two of them.

  • Is it enough to hand the two references over in a fixed order?
    No. Ordering decides which mismatched pair a reader can observe, not whether one exists. With the table published first a reader can hold new routes with old weights; publish weights first and it can hold the reverse. Either way there is an interval in which the two references disagree about which publication is current.
  • When are two separate references still the right design?
    When no single decision reads both, when they are published by different owners whose release cadences should stay independent, or when one half is large and stable and joining them would force it to be rebuilt every time the volatile half changes. The test is whether any one decision depends on both.
  • Does joining them into one composite value cost much?
    Usually very little. The composite holds references to both halves, so the unchanged half is reused rather than copied, and a publication allocates one extra small holder. The real cost is that both halves are now published together, which couples their release cadence even when only one of them changed.

saying these in an interview costs you the question

  • Assumes two immutable values must agree because each is immutable
  • Thinks handing the two references over closer together closes the window
  • Believes a fixed publication order removes the mismatch
  • Calls the mismatched pair a visibility defect rather than a design one
  • Treats one indivisible handover as covering everything a publisher changed
  • Tries to fix it by re-reading one of the two references