Why should a ledger entry's monetary amount not cross the wire as a binary floating-point field?
answer
- denominators must be powers of two
- one tenth never terminates in binary
- tiny errors accumulate across a batch
- minor-unit integer or decimal string
- the currency carries the exponent
basics
~20 sBinary 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.
solid answer
~40 sA binary floating-point field stores a value as a significand times a power of two. A decimal amount such as `0.10` needs a denominator with a factor of five, so its binary expansion never terminates and the stored value is only the nearest neighbour. Each amount is off by a hair, the errors accumulate across a batch, equality comparisons against a literal fail, and a reconciliation total ends a cent away from the sum of its lines. The two honest wire shapes are an **integer count of the currency's minor unit**, or a **decimal string** that a reader parses into a decimal type — both carried alongside the currency, because the exponent that scales the integer differs by currency. Whichever you choose, the contract must also state the scale and the rounding rule.
go deeper
Know the headline and the reason: decimal fractions such as one tenth have no exact binary representation, so money does not belong in a floating-point field. Integers of the smallest unit, or decimal text, are the normal alternatives.
Explain the mechanism — significand times a power of two, a denominator with a factor of five, rounding to the nearest neighbour — and the two working wire shapes with what each needs alongside it.
Show the operational consequence: batch totals that fail to foot, equality rules replaced by tolerances, and logs that disagree because renderers print the same bits differently. Then state where rounding happens and under which rule.
Own the cross-team decision: one amount representation for the whole contract, currency and scale mandatory, rounding rule published, and boundary vectors in the conformance suite so a new consumer cannot quietly reintroduce a float.
## Why the representation cannot hold the value A 64-bit binary floating-point value is a signed significand multiplied by a power of two. A fraction is exactly representable only when its denominator is a power of two: one half, one quarter, one eighth. A monetary amount is a fraction over a power of ten — `0.10` is one tenth, and ten factors into 2 x 5. The factor of five is the whole story: in binary, one tenth is a repeating expansion, so what gets stored is the **nearest representable neighbour**, slightly above or below the value you meant. The classic demonstration is that adding a tenth to two tenths yields a value that renders as `0.30000000000000004` rather than `0.3`. That is not a bug in the addition; both inputs were already approximations, and the sum of two approximations is a third one. ## How the error becomes a defect in a ledger 1. **Drift across a batch.** One entry is off by a fraction of a cent and nobody notices. Ten thousand entries are off by a cent or two, and the total no longer equals the sum computed by the system on the other side of the wire. 2. **Equality stops working.** A rule written as *amount equals twelve point three zero* is false, because the decoded value is not that number. Teams then reach for a tolerance, and the tolerance becomes the new contract. 3. **Rounding hides it, then reveals it.** Display rounds to two places and everything looks right until a reconciliation report compares unrounded sums. 4. **The same bits can be printed differently.** Renderers differ in how many digits they emit for the same value, so one log shows `10.23` and another shows `10.229999999999999` — and a reviewer concludes the two systems disagree about the amount when they hold identical bits. ## The shapes that actually work | Wire shape | What it carries | What must travel with it | Where it strains | |---|---|---|---| | Minor-unit integer | A whole count of the currency's smallest unit | The currency, whose exponent tells the reader how to scale it | Amounts finer than a minor unit, such as unit prices or accrued interest | | Scaled integer | An unscaled integer plus an explicit scale | The currency | Every reader must apply the scale; forgetting it is off by a power of ten | | Decimal string | Digits with a decimal point, as text | The currency, and a reader that parses into a decimal type | Comparing or sorting as text instead of numerically | | Native decimal type | An unscaled integer and a scale inside the encoding | The currency | Not every encoding offers one, so cross-format hops may degrade it | The minor-unit integer is the common default because it is exact, compact and cheap to validate. Its trap is the assumption that a minor unit is always one hundredth: **currency exponents differ** — most currencies divide into hundredths, some have no minor unit at all, and a few divide into thousandths. An integer amount without its currency is therefore not merely unlabelled, it is uninterpretable, and an integer amount whose reader assumes hundredths for a currency that uses thousandths is wrong by a factor of ten. The decimal string is the right answer when the scale varies or exceeds the minor unit, because it carries its own scale in the digits, including significant trailing zeros. Its trap is the reader: a string field that is parsed straight back into a binary float has bought nothing at all. ## What the contract must say - The **currency** field and where it lives relative to the amount. - The **scale**: fixed at the currency's exponent, or an explicit field, and whether trailing zeros are significant. - The **rounding rule and where rounding happens** — which side of the wire rounds, and to what. Half-away-from-zero and half-to-even give different totals over a large batch, and a ledger that must foot to a control total cannot leave that unstated. - The **allowed range**, so an absurd amount is rejected at the boundary rather than absorbed. - A **boundary vector** in the conformance suite: an amount with a significant trailing zero, one in a currency with a non-standard exponent, and one at the maximum magnitude. ## The fix that is not a fix Switching to a wider binary floating-point type does not help. Widening adds bits to the significand, but the denominator still contains a factor of five, so the expansion is still non-terminating and the value is still an approximation — a more precise wrong number. The correction is to change the **kind** of representation, from binary fractions to decimal ones, not to buy more of the wrong thing.
- A team replaces the float with an integer count of minor units. What has to travel with it, and why?The currency. The integer is meaningless without the exponent that scales it, and that exponent is a property of the currency: most divide into hundredths, some have no minor unit, a few divide into thousandths. A reader that assumes hundredths for a thousandths currency is wrong by a factor of ten, and the error is uniform, so it looks like a pricing decision rather than a decode defect.
- When is a minor-unit integer the wrong choice, and what replaces it?When the value is finer than a minor unit — a unit price quoted to four or six places, an accrued interest figure, an exchange rate. Then the minor unit is not the natural scale, and you carry either an unscaled integer with an explicit scale field or a decimal string. Both keep the scale on the wire instead of assuming it.
- Does moving to a decimal string guarantee the consumer keeps the precision?No. It guarantees the bytes carry the exact value, which is the writer's half of the contract. If the reader parses the string straight into a binary float, the same rounding happens one step later and nothing has been gained. The contract has to state the target type, and the conformance suite has to prove a reader round-trips a value with a significant trailing zero.
Recording money in binary floating point is like measuring in thirds of an inch with a ruler marked only in halves and quarters. Each reading is near enough to trust on its own, and the error only becomes visible once you add a few thousand of them and compare against the wall.
saying these in an interview costs you the question
- Claims a wider floating-point type makes decimal amounts exact
- Says comparing amounts within a small tolerance is the proper fix
- Assumes every currency divides into exactly one hundred minor units
- Sends a minor-unit integer with no currency alongside it
- Parses a decimal string straight back into a binary float
- Leaves the rounding rule and its location out of the contract