0.1 + 0.2 is not 0.3. Everyone knows this. It still ships in financial code every year, usually because the first version stored a price and nobody was thinking about a ledger yet.
Why it survives so long
The error is tiny. A few thousand transactions in, you are out by a fraction of a paisa, and nothing breaks. Reports look right. Nobody notices until a reconciliation run compares your total against the bank’s and finds a discrepancy that cannot be attributed to any single transaction — because it is not in any single transaction. It is spread across all of them. A duplicate charge from a retried request at least points at one row you can go and look at; rounding drift points at nothing.
That is the expensive part. Not the error, the investigation.
Store minor units
Keep an integer count of the smallest unit the currency has, plus the currency code:
amount_minor bigint -- 149900
currency char(3) -- 'INR'
That means ₹1,499.00. No decimal exists anywhere in storage or transport. Formatting to two places is a presentation concern and belongs in the view layer, once.
bigint, not int. A 32-bit integer caps around ₹21 crore in paise, which is a limit you will hit eventually and at the worst possible time.
Currencies are not all two-decimal
Hardcoding a divide by 100 breaks the moment you touch a currency with different precision. Kuwaiti dinar has three decimal places. Japanese yen has zero — ¥100 is 100 minor units, not 10,000. Store the exponent alongside the currency and derive the divisor:
INR 2
JPY 0
KWD 3
Division is where it still goes wrong
Integers do not save you from splitting. Divide ₹100 three ways and you have ₹33.33 three times and one paisa unaccounted for. Someone must receive it, and the rule must be written down: largest remainder, first line, or a designated rounding account. Whatever you pick, the sum of the parts equals the whole, and the rule lives in one function every caller routes through. On fintech work that function tends to be the shortest file in the repository and the most argued over, which is the correct ratio.
That is the actual invariant. Not “use integers” — nothing is created or destroyed by arithmetic. It is the same rule double-entry enforces one row at a time; integers just make it checkable inside a single value.