Handling money in software: why floats will eventually cost you
7 min read
Why floating-point numbers can't hold money, how to store and round amounts correctly, and how PostgreSQL, MySQL, Java, Python, JavaScript, Go and PHP handle it.

Type 0.1 + 0.2 into almost any programming language and the answer is 0.30000000000000004. In a physics simulation, nobody cares. In an invoice, it is a cent that doesn't reconcile - and in a system that processes a few million transactions, it is an accountant asking why the totals don't match the bank statement.
Money looks like the simplest data a system handles. It is one of the easiest to get wrong, and the mistakes are expensive precisely because they are small: they pass every test with round numbers and surface months later, in the reports.
Why floats can't hold money
Floating-point numbers (float and double, the IEEE 754 standard used by almost every language) store values in binary. Just as 1/3 has no exact decimal representation, 0.1 has no exact binary one. The computer stores the nearest value it can - slightly more or slightly less than 0.1 - and the error appears as soon as values are added, multiplied or compared.
Individually, the errors are tiny. The problems come from what happens next:
- Comparisons fail.
0.1 + 0.2 == 0.3is false, so a check that an invoice is "fully paid" can quietly fail. - Errors accumulate. Summing thousands of line items drifts away from the correct total.
- Rounding goes the wrong way. A value meant to be exactly 2.675 may be stored as 2.67499999..., so rounding to two decimals gives 2.67 instead of 2.68.
PHP makes this harder to notice. echo 0.1 + 0.2 prints 0.3, because echo rounds to 14 significant digits; var_dump shows the real value. The PHP manual's own example is floor((0.1 + 0.7) * 10), which returns 7, not 8.
Two correct ways to store amounts
There are two reliable approaches, and both avoid floating point entirely.
Integers in the smallest unit. Store 19.99 EUR as 1999 cents. Integer arithmetic is exact, fast and available everywhere. The catch is that the number of decimal places depends on the currency: the Japanese yen has none, the euro has two, and currencies such as the Kuwaiti dinar have three. An amount is meaningless without knowing its currency - so the two are stored together.
A decimal type. Databases and most languages offer a type that stores numbers in base 10 with fixed precision, so 0.1 is exactly 0.1. It is the natural choice when amounts need more precision than the currency's minor unit - unit prices with four decimals, exchange rates, interest calculations.
Many systems use both: decimals in the database, integers or decimal objects in the code. What matters is that no amount ever passes through a float on the way.
How different stacks handle it
Databases. PostgreSQL's numeric and MySQL's DECIMAL are exact, and a column such as numeric(19,4) covers most business needs. FLOAT, REAL and DOUBLE are approximate and don't belong in a money column. PostgreSQL also has a money type, but its output depends on the server's locale setting and it has fixed precision - the PostgreSQL community itself recommends numeric instead.
Java has BigDecimal, which is exact - with two traps. new BigDecimal(0.1) inherits the float error, because the 0.1 is already a double before the constructor sees it; new BigDecimal("0.1") or BigDecimal.valueOf(0.1) is correct. And equals() compares scale, so 2.0 and 2.00 are not equal - compareTo() is the right test.
Python has decimal.Decimal in the standard library, with the same trap: Decimal(0.1) is inexact, Decimal("0.1") is exact.
.NET has a built-in 128-bit decimal type designed for financial calculations.
JavaScript has no decimal type at all - every number is a double. Money in JavaScript means integers in minor units or a library such as decimal.js or dinero.js. Integers are exact only up to 2⁵³ − 1, which is plenty for amounts, but a reason to send large values between systems as strings. A native decimal type has been proposed for the language, but it is not part of it yet.
Go has no decimal type in the standard library either; teams use int64 minor units or a library such as shopspring/decimal.
PHP has the bcmath extension for arbitrary-precision arithmetic on numeric strings, and PHP 8.4 added the BcMath\Number object, which makes it far more pleasant to use. Libraries such as brick/money and moneyphp/money go further and implement the Money pattern: an object that holds the amount and the currency together and refuses to add euros to dollars.
APIs make the same choice. Stripe expects amounts as integers in the smallest currency unit; PayPal sends them as decimal strings such as "10.00". Neither sends a JSON float - and your own APIs shouldn't either.
Rounding is a business decision
Exact storage does not remove rounding - it only moves it to the places where you decide it happens. Three decisions have to be made explicitly:
When to round. Rounding every line of an invoice and summing the lines can give a different total than summing first and rounding once. Neither is wrong in itself; the rules come from the country's tax law and from what accounting expects. The system has to follow one rule, everywhere, and it must be written down.
How to round. "Round half up" (2.675 → 2.68) is what most people learned at school. "Round half to even", also called banker's rounding (2.665 → 2.66, 2.675 → 2.68), reduces bias when many values are rounded and is common in finance. Languages disagree on the default: PHP's round() rounds half away from zero, Python's round() and Decimal round half to even, JavaScript's Math.round() rounds half towards positive infinity (so -2.5 becomes -2), and Java's BigDecimal makes you choose a mode explicitly - the most honest design of them all.
Where the cent goes when you split. 100.00 divided into three payments is not 33.33 three times - that loses a cent. It is 33.34, 33.33 and 33.33. Money libraries provide an allocate function for exactly this, and it is worth using even for "simple" splits like instalments or shared costs.
Currencies and conversion
Amounts in different currencies cannot be added, and a system that stores a bare number without its currency will eventually do exactly that. The Money pattern prevents it by making the currency part of the value.
Conversion needs the same care. Store the original amount, the rate used and the date, not just the converted result - conversions are not reversible once rounded, and someone will ask how a number was calculated.
Bulgaria's move to the euro on 1 January 2026 is a textbook case. Every lev amount is converted at the fixed rate of 1.95583 leva per euro and then rounded to the nearest cent. Converting each price and then summing a basket gives a different result than summing in leva and converting the total, and converting the result back to leva rarely returns the original number. Systems that had stored amounts as floats, or had never defined where rounding happens, found out during the changeover.
A short checklist
- No
floatordoubleanywhere an amount passes through: database, code, JSON, spreadsheet export. - Every amount travels with its currency.
- Integers in minor units or a decimal type - chosen deliberately and used consistently.
- Rounding mode and rounding point defined in one place, agreed with accounting, and covered by tests with awkward values such as 2.675 and 0.005.
- Splits use allocation, so the parts always add up to the whole.
- Conversions store the original amount, the rate and the date.
Money is the one place in software where "close enough" is never close enough. Most of my work on a billing platform at a US payments company came down to these rules - not because they are difficult, but because every one of them is easy to break by accident.