skip to content

questions

6

Why can a 64-bit ledger entry identifier lose its last digits when the consumer decodes every number into a binary floating-point value?

level: middleimportance: must knowfreq 70%

answer

  1. the reader has one number type
  2. wire says integer, reader hears float
  3. 53 significand bits, not 64
  4. exactness stops at 2^53
  5. carry identities as opaque strings

basics

~20 s

A 64-bit binary floating-point value carries only 53 bits of significand, so whole numbers above 2^53 (about 9.0 quadrillion) round to the nearest representable neighbour. The identifier decodes to a different number, with no error raised.

solid answer

~50 s

The wire carries an exact integer, but the consumer's only numeric type is a 64-bit binary float, whose significand is 53 bits wide. Every integer up to `2^53` (9007199254740992) is exactly representable; past that the representable values thin out, so a 19-digit identifier is snapped to the nearest one it can hold and the trailing digits change. Nothing fails loudly: no range error, no exception, and the value still prints as a plausible integer, so the corruption surfaces later as a join that matches nothing or two records that collide. The fix is to stop asking a float to be an integer — carry the identifier as a quoted string and treat it as opaque, or use a wire type that is a distinct 64-bit integer *and* a reader that maps it to an integer type.

code

pseudocode · 14 lines
pseudocode
MAX_EXACT = 9007199254740992    // 2^53

// Guard on the digits, at the trust boundary, before any decode:
function accept_entry_id(text):
    if not all_digits(text):
        reject("entry id is not an integer literal")
    if compare_as_integer_text(text, "9007199254740992") > 0:
        reject("entry id exceeds the exact-integer range of a binary float")
    return text                 // keep it opaque; never compute on it

// Why the same guard cannot be written after decoding:
n = decode_as_binary_float("9007199254740993")
// n is now 9007199254740992, which is equal to MAX_EXACT,
// so a post-decode range test passes and the lost digit is invisible

go deeper

for a junior

Remember the headline: a number type that stores fractions cannot hold every large whole number exactly. Identifiers are names, not quantities, so sending them as text is normal and not a hack.

for a middle

Be able to explain the mechanics: 53 bits of significand, exactness up to 2^53, representable values thinning out above it, and rounding to nearest as defined behaviour rather than an error.

for a senior

Show that you have debugged it. Name the silent symptoms — a join that matches nothing, two identifiers colliding into one — and place the validation on the text at the trust boundary, because a post-decode check cannot see the loss.

for a principal

Frame it as a contract policy question: which scalars are identities, what the weakest consumer's numeric type can hold, and how boundary test vectors in the contract's conformance suite stop the next consumer from rediscovering this in production.

## An exact value meets an approximate type A ledger entry identifier is an **exact whole number**. It is not a quantity you do arithmetic on; it is a name made of digits, and the only operation that matters is equality. The wire carries it fine. The damage happens at the far end, when a consumer whose number model is a single **64-bit binary floating-point type** decodes it. That type stores a value as a sign, an 11-bit exponent and a 52-bit stored fraction, giving **53 bits of significand** once the implicit leading bit is counted. Integers are therefore exact only while they fit in 53 bits: - up to `2^53` = **9007199254740992** (16 digits, about 9.0 x 10^15), **every** integer is exactly representable; - between `2^53` and `2^54`, only **even** integers are representable — the gap is 2; - each further doubling of magnitude doubles the gap again. A 19-digit identifier — anything near the top of a signed 64-bit range, whose maximum is 9223372036854775807 — sits roughly a thousand times past `2^53`. Decoding it into a binary float snaps it to the nearest representable neighbour, which can be hundreds of units away. ## Why it is silent This is the part interviewers are actually probing. Three things conspire: 1. **No error is raised.** Rounding to nearest is the defined behaviour of the type, not a fault condition, so the decoder has nothing to report. 2. **The result still looks like an identifier.** It is a whole number of the same length, so logs, dashboards and eyeballs see nothing. 3. **A post-decode guard cannot catch it.** By the time you hold the number, the evidence is gone; a value that was `2^53 + 1` is now exactly `2^53`, which passes any range test you write against `2^53`. The visible symptoms arrive downstream: a lookup that matches nothing, an idempotency key that stops deduplicating, or — worse — two distinct source identifiers that round to the *same* neighbour and silently merge two entries into one. ## What actually fixes it | Approach | What crosses the wire | Survives a 19-digit id? | What it costs | |---|---|---|---| | Bare number literal | A number the reader types for itself | Only where the reader has an exact 64-bit integer type | Silent rounding everywhere else | | Quoted decimal string | Digits as text, parsed deliberately | Yes | The reader must parse; the schema can no longer range-check it as a number | | Distinct 64-bit integer field in a schema-driven encoding | A typed integer | Yes, if the generated accessor maps it to an integer | Some readers still widen it to a float on the way out | | Two 32-bit halves | Two values, each below `2^32` | Yes, both halves are exact | Every consumer must recombine; easy to get wrong | The string is the blunt, reliable answer and it is the right one for an **identity**: you never add identifiers, so losing arithmetic costs nothing. Declaring the field as a 64-bit integer in a schema is a real improvement, but note what it does and does not do — it constrains the **writer** and the bytes, not the reader's numeric type. Ecosystems differ here: some readers have an exact wide-integer type and will honour the declaration, others have only binary floating point and will widen it regardless, and a few offer an opt-in arbitrary-precision decode. That difference is exactly why the contract, not the schema alone, has to say what the field is. ## What to put in the contract - State the field's **role**: identity (opaque, never computed on) or quantity. - State the **shape on the wire** — quoted digits, or a typed integer — and the maximum magnitude. - Say that the value is **opaque**: no arithmetic, no ordering assumptions, no re-formatting. - Publish a **boundary test vector** — an identifier just above `2^53` — so a new consumer's conformance run fails immediately rather than in production. - Validate on the **text** at the trust boundary, before decoding, since that is the only moment the evidence still exists. ## The trap to avoid The seductive wrong fix is to reach for more precision: a wider float, a higher-precision decode, a re-round at the end. Widening helps only until the next order of magnitude, and it does nothing about the real problem, which is that an approximate type is being asked to carry an exact name. Change the type, not the precision.

  • The schema declares the field as a 64-bit integer. Does that alone protect the value at every consumer?
    No. The schema constrains the writer and the bytes on the wire; it does not change the reader's numeric type. A generated accessor in an ecosystem whose only number is a binary float will widen the field on decode and round it exactly as a text literal would. You need a declared integer type and a reader that maps it to an exact integer, or you carry the digits as text.
  • Two identifiers in one batch decode to the same number. What does that break?
    Anything keyed on identity: joins match the wrong row, deduplication merges two distinct entries, and an idempotency check treats a second entry as a replay of the first. It is worse than the single-value case because the record count silently drops, and both values still print as well-formed identifiers, so the loss looks like a missing message rather than a decode defect.
  • Where exactly does exactness stop, and which identifiers are at risk?
    Every integer up to 2^53 (9007199254740992, 16 digits) is exact. Between 2^53 and 2^54 only even integers are representable, and the gap doubles with each further octave. So 16-digit identifiers sit on the boundary and 18- or 19-digit ones — time-ordered identifiers and anything minted near the top of a signed 64-bit range — are already well past it.

saying these in an interview costs you the question

  • Assumes every 64-bit integer survives any decoder unchanged
  • Expects the decoder to raise an error when precision is lost
  • Proposes a wider floating-point type instead of a different type
  • Believes only fractional values lose precision, never whole numbers
  • Treats quoting as a cure while the reader still parses it into a float
  • Validates the identifier's range after decoding rather than on the text
open as a page

Why should a ledger entry's monetary amount not cross the wire as a binary floating-point field?

level: middleimportance: must knowfreq 75%

basics

~20 s

Binary floating point represents values as fractions over powers of two, so amounts like 0.10 have no exact representation and every decode introduces a tiny error. Carry money as an integer count of minor units, or as a decimal string, with the currency.

open as a page

Two consumers of one ledger feed render the booked-at timestamp in January 1970 and tens of thousands of years ahead — what single defect explains both?

level: seniorimportance: should knowfreq 52%

basics

~20 s

The contract carries a bare epoch integer without naming its unit. One consumer reads a seconds value as milliseconds and lands days after 1970; the other reads a milliseconds value as seconds and lands far in the future. The factor is one thousand.

open as a page

Why does a UTC offset fully pin a ledger entry's booked instant but not a payment scheduled for 09:00 local next year?

level: seniorimportance: should knowfreq 46%

basics

~20 s

An offset is arithmetic about one moment, so a past instant plus its offset is unambiguous forever. A future local time is defined by rules that can change, so it needs a zone identifier and late resolution, not a frozen offset.

open as a page

How do you decide which numeric and temporal scalars in a cross-team wire contract must cross as strings rather than native numbers?

level: principalimportance: should knowfreq 34%

basics

~20 s

Decide per field, not per contract: compare the value's required exactness and magnitude against the weakest consumer's numeric type, and let identities and money cross as text while ordinary quantities stay native. Then enforce it with boundary vectors.

open as a page

Why can two payee names that render identically fail an equality check after crossing a wire contract as text?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

Unicode allows more than one code-point sequence for the same rendered text: a precomposed accented character, or a base letter followed by a combining mark. Both encode faithfully, so equality by bytes or code points fails while the display matches.

open as a page