Sep 2, 2026 · 1 min read
Why money is an integer
The shop stores every price as cents. Here is the floating-point arithmetic that makes any other choice a slow-motion reconciliation failure.
Open a JavaScript console and type 0.1 + 0.2. You get 0.30000000000000004.
That is not a bug in JavaScript. It is what binary floating point is: most decimal fractions have no exact binary representation, so every operation rounds. For a physics simulation that is fine. For a receipt it is a problem, because your payment provider is rounding differently, and the two of you will disagree by a cent on roughly one order in ten — forever.
The rule
Store money as an integer number of the smallest unit. In this codebase that is priceCents:
model Product {
/// Smallest currency unit. $12.50 is stored as 1250.
priceCents Int
currency String @default("usd")
}
Three items at $19.99 is 1999 * 3 = 5997. Exactly. Always.
The edges
Integers only help if the conversion happens in exactly one place at each edge:
- Input. An admin types
12.50.parsePriceInputturns that into1250— and refuses12.505rather than rounding it. Silent rounding is how a product ends up a cent cheaper than intended. - Display.
formatCents(1250)renders$12.50throughIntl.NumberFormat, which also knows that Japanese yen has no minor unit.
Nothing in between ever divides by 100.
What the tests pin down
it("never produces floating-point drift", () => {
expect(lineTotal(1999, 3)).toBe(5997);
});
it("rejects more than two decimals rather than rounding", () => {
expect(parsePriceInput("12.505")).toBeNull();
});
Both of those are one-line tests over pure functions. That is the payoff of keeping the money logic out of the components and out of the database layer: the rule that matters most is the cheapest one to prove.