Why we store money as an integer, never a float
· YoIndie
Open a JavaScript console and type 0.1 + 0.2.
0.1 + 0.2;
// 0.30000000000000004
This is not a bug in JavaScript. Binary floating point cannot represent one tenth, in the same way decimal cannot represent one third. The error is tiny and it compounds, and it compounds in the one place nobody tolerates being approximately right.
The rule
Every amount in Starter Kit, our Next.js SaaS starter,
is an Int, counting the smallest unit of its
currency: paise for rupees, cents for dollars, and yen for yen — which has
no subunit at all, and is the reason the multiplier lives beside the currency
rather than being hardcoded as 100.
| Displayed | Stored | Currency |
|---|---|---|
| ₹499.00 | 49900 | INR |
| $29.99 | 2999 | USD |
| ¥3,000 | 3000 | JPY |
The database column is an integer. The provider APIs already work this way — Stripe, Razorpay and Dodo all take amounts in the minor unit — so the conversion happens once, at the edge, when a number is put in front of a person.
Half a fix is not a fix
The failure mode is not "we used a float and lost money". It is that one function somewhere returns a float, and every arithmetic operation downstream inherits it. A proration that divides by twelve, a tax line that multiplies by 0.18, a total that sums a cart — each one is fine on its own and wrong in aggregate.
So the rule is not "prefer integers". It is that a float never enters. There
is no parseFloat on an amount anywhere in Starter Kit, and the type system
carries the constraint: an amount is a number that is always an integer, and
formatting is the only place it is ever divided.
Formatting, once
Division happens in exactly one place, at render:
new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount / 100);
Intl.NumberFormat knows that yen has no decimal places and that Indian
rupees group as 1,00,000 rather than 100,000. That is a large amount of
per-locale knowledge you get for free, and it is the reason there is no
hand-rolled currency formatter here.
Where to see it
Every price, receipt and refund in Starter Kit follows this rule, from the database column to the checkout with Stripe, Razorpay or Dodo Payments. If you are starting a paid product, it is one less thing to get right on your own.