# 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.

Author: Hasan Misbah
Published: 2026-07-11
Reading time: 6 min read
Canonical: https://misbah.co/blog/store-money-as-integer-cents-never-a-float

---


## TL;DR

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:

```js
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](https://www.moderntreasury.com/journal/floats-dont-work-for-storing-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](https://docs.stripe.com/currencies) 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](/blog/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()`](https://martinfowler.com/eaaCatalog/money.html), which exists precisely so you avoid losing pennies to rounding.

```php
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](/blog/the-refund-path-is-where-i-read-a-codebases-soul), and why proration is where subscription systems quietly leak, in [subscriptions are just licensing that bills on time](/blog/subscriptions-are-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](https://world.hey.com/otar/storing-money-as-integer-cents-is-often-over-engineering-7238a485): 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](https://cardinalby.github.io/blog/post/best-practices/storing-currency-values-data-types/). 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

```sql
-- 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.

## The takeaway

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

**Is a SQL DECIMAL just a float under the hood?**
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.

**Why does Stripe send 1000 for ten dollars?**
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.

**What about currencies with three decimal places, like the Kuwaiti dinar?**
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.

**Can I just round at the end and move on?**
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.

**Integer cents or a money library, which should I pick?**
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.
