In Production · Backend · Fundamentals
0.1 + 0.2 and someone’s salary
In a programming contest, an epsilon makes the bug go away. In payroll, an epsilon is a person who got paid one cent less.
Here is a program that should print true:
double phi = (1 + sqrt(5)) / 2;
printf(1 / phi == phi - 1 ? "true" : "false"); /* false */
The golden ratio satisfies 1/φ = φ − 1 exactly. The computer disagrees in the sixteenth decimal, because the two sides reach the same number through different roundings. In competitive programming the fix is a reflex: compare with a tolerance, fabs(x - y) < 1e-9, and move on to the next problem. I did that for years.
To see why, look at what a double actually holds. 0.1 has no exact binary form, so the machine stores the closest value it can.
Loading interactive figure…
Static version: 0.1 is stored as 0.1000000000000000055511151231257827…
Then the numbers became salaries. Payroll across countries is additions, rates and roundings, millions of times a month, in currencies that do not even agree on how many decimals they have: the Japanese yen has none, the Kuwaiti dinar has three. Every rounding lands in someone’s account, and the totals must reconcile to the last unit. The scenario below is generic, not any company’s system. Add the same amount to a running total, payslip after payslip.
Loading interactive figure…
Static version: 0.10 added 1,000,000 times in float is 100000.00000133288; in integer cents, exactly 100,000.00.
The drift is a tiny fraction of a cent. Round the total and it even looks right, which is the trap. The equality check fails, the reconciliation job raises an alarm, and someone adds an epsilon to silence it. From then on the epsilon is policy.
Where the cents really go
The money that actually goes missing comes from somewhere else: applying a rate, an FX conversion or an uplift, and rounding every line.
Loading interactive figure…
Static version: at rate 1.15, 2,376 of 100,000 lines are off by a cent with floats; with integer cents, none.
At a rate of 1.15, 2,376 of 100,000 lines round to the wrong cent with floating point. With integer cents and a decimal rate, none do. An epsilon cannot fix that, because nothing is “close enough” about a cent paid to the wrong person.
The fix is a representation, not a comparison:
- Store money as an integer count of minor units plus a currency, and take the number of decimals from the currency (ISO 4217), never from a global “2”.
- Do rate math in a decimal type, choose one rounding rule (half-up or half-even), write it down, apply it once at a defined step, and property-test it against exact arithmetic.
- Treat reconciliation as equality. If totals only match with an epsilon, the bug is upstream.