How do you choose the default for a new field so older readers that ignore it stay correct?
answer
- the default is what every older reader acts on
- reproduce the behaviour before the field existed
- an absent field and a lost field look identical
- choose the polarity so loss is conservative
- no safe value means no defaulted field
basics
~20 sPick the value that reproduces the behaviour the system had before the field existed, and check that the same value is safe when the field is lost in transit. If no single value satisfies both, the state does not belong in a defaulted field.
solid answer
~50 sA default is not a convenience — it is **the value every reader that lacks the field will act on**, including readers downstream of a hop that dropped it. Two rules apply. First, the default must be **inert**: it must reproduce the behaviour readers had before the field was added, so adding it changes nothing for anyone who has not upgraded. Second, the default must be **fail-safe in the direction of loss**: since an absent field reads as the default, ask what happens when the writer's real value never arrives, and choose the polarity so that outcome is the conservative one. The two rules usually agree; when they conflict — a flag whose absence must mean "stop" but whose historical behaviour was "go" — the honest move is an enumerated field with an explicit unspecified member that readers refuse to act on, rather than a defaulted boolean.
code
pseudocode · 9 lines# new field holdForReview, implicit default false
producer_out = { orderId: 91, total: 4200, holdForReview: true }
old_hop_view = { orderId: 91, total: 4200 } # field not in its schema
old_hop_out = { orderId: 91, total: 4200, region: "eu" } # re-encoded, field gone
sink_view = decode(old_hop_out) # holdForReview -> false
if not sink_view.holdForReview:
release(sink_view.orderId) # this branch firesgo deeper
Know that adding a field gives every reader that lacks it a default value, and that this value is what such a reader will actually act on.
Explain the inert-default rule and demonstrate the walk: take the new message, remove the fields an older reader lacks, apply defaults, and state the decision it reaches.
Show the loss-direction test as well: reason about what happens when the real value is dropped in transit, and change the field's shape when no single value is both inert and fail-safe.
Own the standing rule at schema review — every added field states what a reader does when it never arrives — and decide when a change needs an ordered rollout instead of a default.
## A default is a decision, not a placeholder When a field is added to a wire contract, the value chosen as its default is acted on by more readers than the ones that never upgraded. It is the effective value for: - every reader whose schema version predates the field; - every reader downstream of a hop that decoded and re-emitted the message without preserving fields it did not model; - every message written by a producer that has upgraded but has nothing to say about the field. So the design question is not "what is a sensible starting value" but "what should happen when this field is missing, wherever the absence came from". ## Two rules, and the conflict between them 1. **The inert-default rule.** The default must reproduce the behaviour the system had before the field existed. If adding a field changes what an unchanged reader does, the addition is not the compatible change it appears to be — it is a behaviour change smuggled in as a schema edit. 2. **The loss-direction rule.** Because an absent field reads as the default, the default is also what a reader sees when the true value is dropped somewhere upstream. Choose the polarity so that this outcome is the conservative one. Most of the time both rules point the same way, because the pre-existing behaviour is also the cautious one. They diverge exactly when the new field exists to *restrain* something that used to be unrestrained. ## Walking an old reader over new bytes The check is mechanical, and it is the check an interviewer wants to watch you perform: - Write down the new message as the upgraded producer emits it. - Cross out every field the older reader does not model. - Apply the default rule to whatever is now missing. - Read the reader's decision logic against that value and say out loud what it does. If the sentence you say out loud is wrong in a way that matters — an order released that should have been held, a charge applied that should have been waived — the default is wrong, whatever the schema review said. ## Comparing candidate defaults | Candidate default | Reproduces pre-field behaviour? | Safe when the field is lost? | Verdict | |---|---|---|---| | Type zero on a counter that used to start at zero | Yes | Yes — a lost counter reads as fresh | Use it | | Empty list of overrides | Yes | Yes — no overrides applied | Use it | | `false` on a new hold-for-review flag | Yes — nothing was held before | No — a lost hold releases the order | Change the design | | First enumerated member that means a real mode | Only by luck | No — silence reads as that mode | Reserve an unspecified member | | A value chosen because most traffic carries it | No | No | Never | ## When no safe default exists If the two rules genuinely conflict, stop trying to pick a value and change the shape of the field: - **Use an enumerated field whose first member is an explicit `unspecified`**, and write into the contract that readers must not act on it — they escalate, queue, or fall back to a slower authoritative lookup instead of guessing. - **Invert the field** so that the conservative outcome is the default. A field named for the permissive state (`releaseApproved`) defaults to "not approved", which is safer than a field named for the restrictive state (`holdForReview`) defaulting to "no hold" — at the price that every producer must now speak up for the common case. - **Make it a contract change with a rollout order**, upgrading readers before producers begin to emit the field, rather than relying on a default to cover the window. ## The review question The cheap test at schema-review time is one sentence: *"If this field never arrives, what does the reader do, and is that acceptable?"* Asking it turns a default from a box someone filled in into an explicit statement of failure behaviour. Note that defaults and unknown-field policy are one subject, not two: a fail-safe default is what makes a hop that drops unknown fields survivable, and a preserving pipeline is what makes an un-fail-safe default tolerable. A team that decides one without the other has decided neither.
- Why is inverting a flag's polarity a real fix rather than a cosmetic one?Because the default follows the name. A field expressing the permissive state defaults to "not permitted", so both an old reader and a hop that dropped the field end up in the conservative branch. The cost is that every producer must now set it for the common case, which is a deliberate trade, not a free one.
- What does an explicit unspecified enumerated member buy that a defaulted boolean cannot?A third state. A boolean has only two, so absence must be spent on one of them and will be acted on. An unspecified member lets the contract say "the writer did not decide", and readers can escalate or fall back to an authoritative lookup instead of guessing in the dangerous direction.
- How do defaults and unknown-field policy interact?They cover for each other. A fail-safe default makes a hop that drops unknown fields survivable, and a pipeline that preserves unknown fields makes an imperfect default tolerable. Teams that pick one and ignore the other are relying on a property nobody wrote down.
saying these in an interview costs you the question
- Picks the default from what most production messages carry
- Assumes adding a field with a default is automatically a safe change
- Forgets that a dropped field and an absent field read identically
- Treats the first enumerated member as a neutral placeholder
- Says documentation can tell readers to ignore the default