Never Store Money as a Float
0.1 + 0.2 = 0.30000000000000004
If your payment system stores amounts as floats or doubles, this is your codebase.
This isn't academic. I've worked on payment infrastructure where small rounding discrepancies accumulated into real reconciliation headaches. Refunds off by a cent. Totals that don't add up. Accounting teams asking questions you don't have good answers for. All of it traceable back to one decision made early in the data model.
Why Floats Are Wrong for Money
I'm not going to spend paragraphs on IEEE 754. Here's the short version.
Floating point numbers can't represent most decimal fractions exactly in binary. 0.1 in binary is a repeating fraction -- like 1/3 in decimal. The computer stores the closest approximation it can. For most math, that approximation is fine. For money, it compounds.
The key insight: money is not continuous math. $12.50 is not a measurement that needs floating point precision. It's a discrete count of cents -- 1250. Treating it as anything else introduces errors that don't exist in the real world.
What Goes Wrong in Practice
The accumulation problem
Process 10,000 transactions at $0.10 each. Float math may give you $999.9999999999998 instead of $1,000.00. Your ledger doesn't reconcile. Your accounting team is unhappy. You're debugging arithmetic.
The display problem
$12.999999999999998 formatted as a price. Or worse -- rounded to $13.00 when the actual charge was $12.99. A one-cent error on a single transaction is annoying. Across a million transactions, it's a liability.
The comparison problem
# This can fail even when amounts "should" be equal
if transaction.amount == expected_amount:
mark_as_reconciled()
Two floats that represent the same dollar amount may not be equal due to different calculation paths. I've seen this break reconciliation logic in production -- transactions stuck in an unreconciled state because 49.99 computed two different ways produced two different float representations.
The refund problem
Partial refund logic that uses float arithmetic can produce a refund amount that's off by a fraction of a cent. Most payment processors reject amounts that aren't whole integers. Your refund silently fails, and now you've got a customer support ticket and a confused customer.
The Fix: Store Cents as Integers
Simple rule, no caveats needed.
Store all monetary amounts as integers representing the smallest currency unit. For USD: cents. For GBP: pence. For JPY: yen (already a whole unit).
$12.50becomes1250$0.99becomes99$1,000.00becomes100000
In your database:
-- Right
amount INTEGER NOT NULL -- stored in cents
-- Wrong
amount DECIMAL(10,2)
amount FLOAT
Why not DECIMAL? Many developers reach for DECIMAL thinking it solves the float problem. It does solve the binary representation issue, but it still requires your application to handle decimal arithmetic correctly. It introduces the question of "how many decimal places?" which varies by currency. Integer cents is simpler, faster, and unambiguous.
Working With Integer Amounts in Code
Conversion only happens at the edges -- input and display.
# Input: convert from user-facing decimal to integer cents
def dollars_to_cents(amount: str | float) -> int:
from decimal import Decimal
return int(Decimal(str(amount)) * 100)
# Output: convert from integer cents to display string
def cents_to_dollars(amount_cents: int) -> str:
return f"${amount_cents / 100:.2f}"
// PHP equivalents
function dollarsToCents(string $amount): int {
return (int) bcmul($amount, '100', 0);
}
function centsToDollars(int $cents): string {
return '$' . number_format($cents / 100, 2);
}
When you need to do arithmetic on monetary amounts -- splitting bills, calculating percentages, applying discounts -- use bcmath in PHP and Python's decimal.Decimal module. Not native floats. This is the one place where you work in decimal space before converting back to integers.
Multi-Currency Gotcha
Different currencies have different subunits. USD, EUR, and GBP use cents (2 decimal places). JPY uses whole yen (0 decimal places). KWD uses fils (3 decimal places).
A robust system needs to know the currency's exponent to convert correctly. ISO 4217 defines this. Practical advice: store the currency code alongside every amount. Never store an amount without knowing what currency it's in.
class Money:
amount: int # in smallest unit (cents, pence, etc.)
currency: str # ISO 4217 code: "USD", "GBP", "JPY"
What Stripe Does (And Why It Matters)
Stripe's API accepts and returns all amounts as integers in the smallest currency unit. This isn't accidental -- it's a deliberate API design decision made by people who process billions of transactions.
When the payment processor you're integrating with uses integers, and your internal storage uses floats, you're introducing a conversion step that has no reason to exist. Match the representation of the external system you're integrating with.
The Bottom Line
The float mistake is common, easy to make, and surprisingly hard to detect in testing -- because the errors are small and intermittent. Fixing it at the data model level, before you write any business logic, costs nothing and eliminates an entire class of bugs.
Store cents. Use integers. Move on to the hard problems.