
Store money as integer cents, never a float
An opinionated rule for money code: store amounts as integer minor units, never a binary float, and never divide a total to get its parts.
Represent money as an integer count of the smallest currency unit, 1234 for $12.34, never a binary float. Floats cannot hold most decimal amounts exactly, so they leak fractions of a cent that turn into a settlement that does not reconcile.
Store money as an integer count of the smallest currency unit. 1234 means $12.34. Do not use a binary float, ever, for an amount you will add, compare, or refund. This is not a style preference. It is the difference between books that reconcile and a slow leak you only find when someone audits settlement.
Why a float cannot hold $2.78
IEEE 754 binary floats store numbers as sums of powers of two. Most decimal fractions, including 0.1 and 2.78, have no exact binary form, so the machine keeps the nearest approximation. The famous demonstration is one line in almost every language:
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
PHP, Python, and Java doubles do the same thing. The value looks right when you print it with two decimals, which is exactly why this bug survives to production. The moment you add, multiply, or compare these amounts, the tiny error compounds, and as Modern Treasury explains in their writeup on why floats do not work for cents, every workaround (round to two places, compare within an epsilon) is a patch on a representation that was wrong for money from the start.
Integer minor units, the boring correct default
Store the amount as a whole number of the smallest unit the currency has. $12.34 is 1234. This is what Stripe does: every amount in its API is a positive integer in the smallest currency unit, so 1000 charges ten dollars, and 10 charges ten yen, because JPY is a zero-decimal currency with no minor unit. Integers add, subtract, and compare exactly. A database BIGINT is cheap, unambiguous, and immune to the epsilon dance.
The single discipline this asks of you: format to a decimal string only at the very edge, when you render to a human or hand the amount to a gateway. Everywhere in between, it is an integer.
The rule that actually saves you: never divide a total
The float question gets the attention, but the bug I see more often is dividing a total to get its parts. Split $10.00 three ways by dividing and you get 333 plus 333 plus 333, which is 999, and a cent has vanished into the floor function. I walked through this exact failure in a payments context in six payments edge cases that bite at 2am.
The fix is to allocate, not divide: give each part the floor, then hand out the remainder one unit at a time until it is gone. Martin Fowler named this decades ago as the Money pattern’s allocate(), which exists precisely so you avoid losing pennies to rounding.
function allocate(int $totalCents, int $parts): array {
$base = intdiv($totalCents, $parts);
$remainder = $totalCents - $base * $parts;
$result = array_fill(0, $parts, $base);
for ($i = 0; $i < $remainder; $i++) {
$result[$i]++; // distribute the leftover, one cent at a time
}
return $result; // 1000 over 3 -> [334, 333, 333], sums back to 1000
}
This is the same discipline that keeps a refund honest and keeps a mid-cycle plan change honest. It is why I read the refund path first when I audit a codebase, in the refund path is where I read a codebase’s soul, and why proration is where subscription systems quietly leak, in subscriptions are just licensing that bills on time.
The honest counterargument
There is a reasonable objection, argued well in Storing money as integer cents is often over-engineering: integer cents can be more ceremony than a small app needs, and an exact DECIMAL column with a money library does the job. I agree with half of it. A SQL DECIMAL (or NUMERIC) is exact, fixed-point, base-10 arithmetic, not a float, and every serious database has had it for decades. If your stack has a first-class decimal type and you use it consistently, you will not leak money.
What I will not accept is a binary float, and I will not accept deriving a part by dividing a whole. So pick one exact representation, integer minor units or a real decimal type, commit to it, and never mix the two in the same column.
What this looks like in a schema
-- good: exact, unambiguous, integer minor units
amount_cents BIGINT NOT NULL, -- 1234 = $12.34
currency CHAR(3) NOT NULL, -- 'USD'
-- also fine, if you commit to it everywhere
amount NUMERIC(14, 2) NOT NULL,
-- never
amount DOUBLE PRECISION -- a slow leak with a two-decimal disguise
Store the currency next to the amount. An amount without its currency is Fowler’s other warning: sooner or later you add dollars to yen and nobody notices until the total is wrong.
Money is a count, not a measurement, so represent it as an integer of the smallest unit, keep the currency beside it, and never derive a part by dividing a whole. Do that and your refunds, proration, and reconciliation stay honest. Skip it and you ship a leak that hides until the numbers stop matching at settlement, which is the most expensive place to find it.
FAQ
No. DECIMAL and NUMERIC are exact fixed-point base-10 types, while FLOAT and DOUBLE PRECISION are binary approximations. DECIMAL is a safe way to store money; a binary float is not.
Because it represents every amount in the currency’s smallest unit. Ten dollars is 1000 cents; ten yen is 10, because JPY is a zero-decimal currency with no minor unit.
Store the correct number of minor units for that currency, which is 1000 per dinar, not 100. Do not hard-code two decimals; look up the currency’s exponent.
Rounding at the end does not undo a float that was already wrong at every intermediate step, and it silently drops remainders. Allocate the remainder explicitly instead of letting a division swallow it.
Either, as long as you are consistent. A first-class exact decimal type wrapped in a money object is fine, and integer minor units are fine. Mixing them, or reaching for a binary float, is what gets you.