skip to content

Which changes to a scalar field's declared type survive a mixed-version rollout, and which corrupt values without raising an error?

level: seniorimportance: should knowfreq 46%

answer

  1. does the byte form change?
  2. widening helps one direction only
  3. old readers still hold the old width
  4. range is the real contract
  5. reinterpretation is not conversion

basics

~20 s

Only a widening whose on-wire byte form is unchanged survives, and only while writers stay inside the old range. Narrowing, signed-to-unsigned reinterpretation and any edit that changes the byte form corrupt values silently rather than failing.

solid answer

~60 s

Ask two questions in order. **Does the byte form change?** If the encoded representation of a value differs between the two types — fixed-width against variable-length, a number against its text form — then every reader on the other schema misparses, and the only mercy is that the shapes often differ enough to fail loudly. **If the byte form is identical, which direction are you going?** Widening the declared range is safe for a new reader over old bytes, because every old value fits. It is *not* safe for an old reader over new bytes: that reader is still built against the narrower type, so the first value needing the extra range wraps or truncates in its hands. A widening is therefore inert until a writer actually emits a big value, which makes it a promise about writers rather than a property of the schema. Narrowing is breaking in every family, and reinterpreting a signed field as unsigned is the quietest break of all — the same bytes, a different meaning.

code

pseudocode · 5 lines
pseudocode
// writer's schema declares a 64-bit count; a reader's schema was narrowed to 32 bits
written  = 4294967296          // 2^32, one past the 32-bit unsigned range
narrowed = written mod 2^32    // the reader keeps only the low 32 bits

// narrowed = 0  -- the count reads as zero, and nothing reports an error

go deeper

for a junior

Recall that a field's declared type is part of the contract, not a local detail: changing it changes how software on the other side of the wire interprets bytes that have already been written.

for a middle

Explain the two-question routine — first whether the byte layout changes, then which direction the range moves — and say what an out-of-range value actually does when it reaches a reader built against a narrower type.

for a senior

Demonstrate that you check the direction you would rather not. Walk an old reader over new bytes, name the first value that breaks it, and describe the distributional evidence a silent sign or width mismatch leaves behind.

for a principal

Frame it as risk pricing: a type edit that cannot be made safe in both directions should become a new field, because a per-message distinction is cheaper to reason about than a per-deployment one that no schema can enforce.

## Two questions, in order A type edit is not one decision but two, and reviewers who only ask the second one ship silent corruption. 1. **Does the on-wire byte form change?** A type is a pair: a set of values and a way of laying them down. If the layout changes — a fixed-width field becomes a variable-length one, a number becomes its decimal text, an integer becomes a floating-point value — then no reader on the other side is parsing what it thinks it is parsing. This is a breaking edit regardless of direction. 2. **If the layout is identical, which direction does the range move?** Now it is about values, not bytes, and the two decode directions give different answers. ## The table a reviewer keeps in their head | Type edit | New reader over old bytes | Old reader over new bytes | Verdict | |---|---|---|---| | Narrow integer to wider integer, byte form unchanged | every old value fits | truncates or wraps once a large value appears | half-safe: safe only while writers stay in the old range | | Wider integer to narrower integer | out-of-range values wrap, truncate or fail | fits | breaking | | Signed to unsigned at the same width | negative values reappear as large positives | mirror image | breaking, and completely silent | | Integer to floating point | exact up to a binary significand's limit, lossy above it | the same loss | breaking for large magnitudes | | Number to its text form | byte form changes entirely | the same | breaking | | Fixed-width to variable-length over the same range | byte form changes | the same | breaking | ## Why even the safe row is only half safe Widening is the edit everyone calls safe, and the claim is true only for the direction people check. Suppose a count is declared narrower today and the schema widens it. A **new** reader meeting old bytes is fine: every value the narrow type could hold fits in the wider one. An **old** reader — still deployed, still built against the narrow type — meeting new bytes is fine too, right up until a writer emits a value that needs the extra range. At that moment the old reader has nowhere to put it, and depending on the family it wraps to a small or negative number, truncates to the low bits, or fails. So the real content of "widening is safe" is: *safe as long as no writer uses the new range*. That is a promise about producer behaviour, not a property of the schema, and it is unenforceable from the schema file. Treat a widening as two events: the declaration edit, which is harmless, and the first oversized value, which is the actual rollout. ## The quiet ones Two edits deserve singling out because they produce no signal at all: - **Signed to unsigned, same width.** The bytes do not change; the interpretation does. A value of −1 in a 32-bit two's-complement field re-read as unsigned is 4294967295. Nothing is malformed, nothing is out of range, and a threshold check downstream sees an enormous positive number where a small negative one was written. - **Integer to floating point.** A binary floating-point value carries a significand of fixed width; beyond 2⁵³ = 9007199254740992 a binary64 value cannot represent every integer, so large identifiers and counters round to a neighbour. Round-tripping such a value through the wider-looking type loses information while appearing to widen it. Both are cases where "wider" in one dimension is narrower in another, which is why the two-question routine starts with the layout rather than with the range. ## What to do instead - **Establish the actual range that crosses the wire**, not the range the type allows. A narrowing is breaking even when today's traffic fits, because messages already written and writers not yet upgraded are part of the contract. - **For any edit that changes the layout or the meaning, add a new field** and retire the old one, so the two interpretations are distinguishable per message instead of per deployment. - **Do not rely on validation to catch it.** A value that wrapped is a valid value of its type; there is nothing for a range check to reject unless the check encodes business limits rather than type limits. - **Expect families to differ on out-of-range behaviour.** Some wrap, some saturate, some refuse to decode. Depending on which one you get is depending on an ecosystem detail, and the safe posture is to assume the silent one.

  • Why is widening an integer field only half safe?
    Because the direction people check is the reassuring one. A new reader handles every old value, but old readers are still built against the narrower type and have nowhere to put a value that needs the extra range. The edit is inert until a writer emits one, so its safety is a statement about producer behaviour rather than about the schema.
  • What makes changing a number to its text form look safe and behave badly?
    Text feels like a superset — it can spell any number — but the layout changes completely, so readers on the other schema parse the wrong bytes rather than a wider value. Comparison and ordering change too: text sorts lexicographically, so downstream ranges, thresholds and index lookups quietly stop meaning what they meant.
  • Two services disagree about a field's sign after a schema edit. What would you expect to see in the data?
    Values clustered near the top of the unsigned range where negatives used to be — a small negative becomes a very large positive at the same width, with identical bytes on the wire. Nothing is malformed, so the signature is distributional: a bimodal spread with a second cluster at the range's ceiling, appearing at a deploy boundary.

Reprinting a form with a wider box for the amount does nothing until somebody writes a bigger number in it — and the clerks still holding the old forms have nowhere to copy it.

saying these in an interview costs you the question

  • Calls widening safe without asking which side reads the large value
  • Thinks a wider declared type re-encodes messages already written
  • Treats a number-to-text change as a widening because text holds anything
  • Assumes an out-of-range value fails loudly rather than wrapping
  • Believes signed to unsigned is a relabelling with no value change
  • Justifies a narrowing from the range in today's traffic