YoIndie
← Blog

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.

DisplayedStoredCurrency
₹499.0049900INR
$29.992999USD
¥3,0003000JPY

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.