Float for money anti-pattern
Learn why floating-point arithmetic silently corrupts financial calculations, how to use fixed-point integers or DECIMAL types safely, and why every cent counts in a payment system.
TL;DR
- IEEE 754 floating-point cannot represent many decimal values exactly.
0.1 + 0.2 === 0.30000000000000004in JavaScript. This is not a language bug, it is how binary floating-point arithmetic works. - In financial systems, these tiny rounding errors accumulate. Multiplied across millions of transactions, a $0.0000001 rounding error becomes a compliance nightmare.
- The correct alternatives: integer cents (store $9.99 as 999), DECIMAL/NUMERIC SQL type (exact fixed-point), or a decimal library (Python's
Decimal, Java'sBigDecimal). - Every major payment processor (Stripe, PayPal) stores monetary amounts as integers in the smallest currency unit and specifies the currency to determine the divisor.
The Problem
A developer writes the most natural code in the world:
// JavaScript Node.js backend, payment calculation
const subtotal = 19.99;
const tax = subtotal * 0.08; // 8% tax
const total = subtotal + tax;
console.log(total); // 21.589200000000002
You round to two decimal places and store 21.59 in your database. Your finance team expects 21.59. The customer's receipt shows $21.59. Everything looks fine.
Until you run end-of-month reconciliation. 2.3 million transactions, each with a tiny rounding error. Your system shows total revenue of $48,732,419.12. Your bank shows $48,732,419.08. A $0.04 discrepancy triggers an audit. The audit finds hundreds of individual transactions with $0.001 rounding errors. Your financial records are inaccurate, your books won't close cleanly, and SOX compliance is now a problem.
I saw this exact failure at a fintech company in 2022. The root cause took two engineers three weeks to trace because the rounding error per transaction was invisible. It only became visible in aggregate.
Why It Happens
Developers reach for float or double because it is the default numeric type in every language. When you type 19.99 in JavaScript, Python, Java, or Go, the runtime stores it as an IEEE 754 double. Nobody has to opt in. It just happens.
Binary floating-point uses powers of 2 as the basis. The decimal 0.1 cannot be represented exactly in binary, the same way 1/3 cannot be represented exactly in decimal (0.333...). The binary representation is 0.0001100110011... repeating.
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
This is not a Python bug. It is the same in every language that uses IEEE 754 double-precision floats (JavaScript, Java float/double, C/C++ float/double, Go float64).
The individually-reasonable decisions that lead here:
- "The language handles numbers for me." True for counting integers. Dangerously wrong for decimal fractions.
- "I'll round at the end." Rounding a wrong number does not make it right. The accumulated error from upstream calculations carries through.
- "The error is tiny." Per transaction, yes. Across 2 million transactions per month, it is an audit finding.
- "Our tests pass." Your tests check one transaction at a time. The bug is only visible in aggregate.
How to Detect It
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.