18 June 2026
Integers all the way down
Why there is no floating-point number anywhere in the money path of anything I build, and what replaces it.
There is a class of bug that never shows up in your tests, never reproduces locally, and surfaces three months into production as a slow, unexplained drift between what your ledger says and what your payment provider says.
It usually starts with a float64.
The failure mode
Floating point cannot represent 0.1. It represents something extremely close to 0.1,
and the error is deterministic but not intuitive. Add enough of those together —
multiply them by a payout multiplier, apply a percentage rake, split a pot three ways —
and you accumulate a residue.
The residue is tiny. That is exactly the problem. It is too small to notice in a single transaction and too large to ignore across a million of them, and by the time reconciliation flags it you have no idea which transactions were wrong.
Worse, in regulated gaming the drift is not just an accounting nuisance. A payout table that does not sum to the certified return-to-player figure is a compliance failure, regardless of how small the discrepancy is.
The rule
Money is an integer count of minor units. Always.
Not "usually". Not "except for this one percentage calculation". The moment a float appears anywhere in the path — computing a bonus, applying a multiplier, splitting a payout — you have reintroduced the whole problem, because the value will be converted back to an integer eventually and that conversion is where the error crystallises.
In Go this means int64 cents, or int64 of whatever the smallest indivisible unit is
for your currency. In the slot engine I built for
Neon Nights, the game core has a hard invariant: no floating-point
type appears in any function that touches balance, stake, or payout. It is enforceable
by reading the code, which is the point.
But percentages
The objection is always the same: some calculations are genuinely fractional. A 3.5% rake. A 96.2% RTP. A payout multiplier of 2.5×.
Those are fine — you just do the division last and decide explicitly what happens to the remainder.
// rake in basis points: 350 = 3.50%
func applyRake(amount int64, rakeBps int64) (net, rake int64) {
rake = amount * rakeBps / 10_000 // truncates toward zero
return amount - rake, rake
}
Multiply first, divide once, and the truncation happens in one known place instead of being smeared across every intermediate step. Then answer the question the float was letting you avoid: who gets the remainder? The house? The player? A rounding account?
That question has a correct answer in your jurisdiction and your certification documents. A float lets you ship without ever asking it.
Representing the split
For anything where the remainder must be distributed rather than absorbed — splitting a pot, allocating a jackpot contribution — the pattern is largest-remainder:
- Compute each share by integer division
- Sum the shares and compare against the total
- Distribute the difference, one unit at a time, to the parties with the largest truncated remainders
It is more code than total * 0.333. It is also the only version that guarantees the
parts sum to the whole, which is the property you actually needed.
At the boundary
The last place floats sneak in is serialisation. JSON has one number type, and it is a
double. 1234.56 in a JSON payload is already a float by the time your parser sees it,
and if your API contract says "amount is a decimal number" you have pushed the problem
onto every consumer.
Send minor units as an integer, with the currency alongside:
{ "amount": 123456, "currency": "AMD", "exponent": 2 }
Format for humans at the very edge — in the template, in the UI, after every calculation is finished. Never before.
Why this is worth being dogmatic about
Most engineering rules deserve exceptions. This one mostly does not, because the cost asymmetry is severe: the discipline costs you a small amount of ergonomics on every transaction, and violating it costs you an unfalsifiable accounting discrepancy that you will debug under pressure with real customer money involved.
I would rather write applyRake than explain drift to an auditor.