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.
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.
That is the actual invariant. Not “use integers” — nothing is created or destroyed by arithmetic. Integers just make it checkable.