Skip to content
All posts

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. parsePriceInput turns that into 1250 — and refuses 12.505 rather than rounding it. Silent rounding is how a product ends up a cent cheaper than intended.
  • Display. formatCents(1250) renders $12.50 through Intl.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.

Comments

Nothing yet. Be the first.

    Sign in to comment.