How do you decide whether a Go service carries money as int64 minor units or float64?
answer
- who owns the one-cent discrepancy
- exact addition versus a tolerance
- not every currency has two decimals
- the wire type outlives the internal type
- round once, at a named boundary
basics
~20 sDefault to int64 minor units for anything summed, compared or reconciled, and treat the exported scale as a contract with the finance-data and API owners rather than a formatting detail. Reserve float64 for measurements nobody reconciles to the cent.
solid answer
~50 sThe question is not really "which type is more accurate" — it is who owns the discrepancy when two systems disagree by a cent. My default is `int64` minor units with an explicit scale, because addition and equality are then exact and a ledger can reconcile against another system's ledger without a tolerance. `float64` is defensible only for values nobody reconciles — a rate, an estimate, a chart series. The decision that actually needs sign-off is the *exported* one: the scale in the schema, whether the wire type is a JSON number or a decimal string, what rounding rule applies and at which single point it is applied. Those are visible to consumers, and the finance-data owner or API reviewer can and should overrule me on them. I also budget for the migration cost up front, because changing a monetary field's representation after consumers exist means dual-writing and a reconciliation backfill — which is why the call is made before the first consumer, not after the first mismatch.
code
go · 7 linesfunc formatCents(cents int64) string {
sign := ""
if cents < 0 {
sign, cents = "-", -cents
}
return fmt.Sprintf("%s%d.%02d", sign, cents/100, cents%100)
}go deeper
Know the rule of thumb and the reason: money is carried as whole minor units, such as an int64 of cents, because adding and comparing integers is exact while adding floats is not.
Explain the mechanics you would implement: one type holding the amount and its currency, integer arithmetic throughout, and formatting for display in a single place using integer division rather than a float verb.
Show where the representation leaks into operations: reconciliation against another system, aggregate columns that exceed a float's exact integer range, and the rule that rounding happens once at a named boundary rather than on each output path.
Own the decision and its review: state what the exported scale and wire type commit to, name who can overrule you on them, price the migration if the call is wrong, and put the rounding rule in writing before the first consumer exists.
## Frame it as an ownership question, not a type preference Every engineer can recite that floats are inexact. That is not the decision. The decision is: **what does this service promise about a monetary quantity, to whom, and who is accountable when the promise and another system's promise disagree by one cent?** Once framed that way, the type falls out of the contract rather than the other way round. ## The default, and why Carry money as an integer count of minor units — `int64` cents — with the scale recorded explicitly rather than assumed. Reasons, in the order they actually bite: - **Addition and equality are exact.** Summing a million rows introduces no residual, and `a == b` means the amounts are the same amount. With `float64`, both statements need a tolerance, and the tolerance becomes a parameter somebody has to defend. - **Reconciliation is possible at all.** A ledger compared against a bank's or a payment processor's ledger must match exactly. A tolerance-based match is a policy decision with a fraud surface, not an implementation detail. - **Range is a non-issue.** `int64` holds about 9.2 quintillion minor units; even at two decimals that is far past any plausible total. `float64` loses integer exactness above 2^53, which real aggregate columns can reach in minor units. - **The rounding conversation happens once.** With integers, rounding only occurs where you divide — tax, splits, interest — and each of those is a place where somebody must state a rule anyway. `float64` is the right answer for quantities nobody reconciles: an exchange rate, a projection, a dashboard series, a machine-learning feature. Saying so explicitly is part of the judgment — blanket bans produce awkward code where a rate becomes an integer with an invented scale. ## The parts that are somebody else's call I make the internal representation call. These I propose and get signed off, because they are visible outside the service and expensive to change: - **The scale in the exported schema.** Two decimals is not universal: some currencies have no minor unit and some have three. A field hardcoded to hundredths is a bug for a subset of the world, and the correct model carries the amount, the currency, and the exponent it is scaled by. - **The wire type.** A JSON number is convenient and dangerous: many decoders parse it straight into a 64-bit float, so an integer number of minor units is safe only while it stays under 2^53, and a decimal number is not safe at all. A decimal *string*, or an integer minor-unit field plus a currency field, survives any consumer. That choice belongs to whoever owns the API contract. - **The rounding rule and its single location.** Half-up, half-even, or always-toward-the-payer are business rules with regulatory weight in some domains, and rounding must happen once, at a named boundary, on a value that then becomes authoritative. Rounding in two places, or rounding on output while keeping a full-precision value in memory, produces two defensible totals for the same thing. - **What totals mean.** The sum of rounded parts and the rounded sum are different numbers. Which one the report shows is a business answer. ## The migration cost is why this is decided early Changing a monetary field's type or scale after consumers exist is not a refactor. It is: add the new field, dual-write both, migrate consumers one at a time, backfill history, reconcile the old and new columns over a full reporting period, then remove the old field. Months of calendar time and a reconciliation report nobody wanted to own. Pricing that out during design is what converts "floats are probably fine for now" into a decision somebody is willing to be wrong about in writing. ## What Go gives you, and what it does not The standard library has no decimal type. It offers `int64` arithmetic, and `math/big` — whose rational type does exact decimal arithmetic and can render a fixed number of decimal places, and whose arbitrary-precision float can be given an explicit precision. Both are heavier than integers and neither is a drop-in money type. In practice the shape that works is a small internal type wrapping an `int64` of minor units together with its currency, with parsing and formatting concentrated in one file so the rounding rule has exactly one home. Formatting for display is then integer division and a `%02d`-style pad, not a float verb. ## How I would present the decision One page: what the field means, whether anything reconciles it, the proposed internal representation, the proposed wire representation with its scale, where rounding happens, the migration cost if we are wrong, and the one question for the reviewer — typically "is any consumer going to sum these in a spreadsheet or a JavaScript client?", because the answer decides the wire type on its own. If the finance-data owner overrules the scale or the API reviewer insists on a string, that is the process working; the expensive failure is shipping the representation without ever having named it.
- Why is a JSON number a risky wire type for a monetary amount?Many decoders, including JavaScript's, parse every JSON number into a 64-bit float. An integer count of minor units survives only while it stays under 2^53, and a decimal amount does not survive at all. A decimal string, or an integer field plus an explicit currency and scale, is safe for any consumer.
- What breaks in a money type that hardcodes two decimal places?Currencies whose minor unit is not one hundredth — some have none and some have three. The amount, the currency and the scaling exponent belong together; a bare integer whose scale is folklore breaks the first time the service handles a currency somebody assumed away.
- When is float64 the right choice for a money-adjacent value?When nothing reconciles it to the unit: an exchange rate, a forecast, a chart series, a model feature. The test is whether anyone will ever compare it for equality with a number produced by another system. If not, the precision of a float is fine and an invented integer scale just adds friction.
- You inherit a service that already stores amounts as float64. What do you do?Measure before rewriting: reconcile against the authoritative ledger and find whether the discrepancy is real and growing. If it is, add the integer field, dual-write, migrate consumers one at a time, backfill, and reconcile over a full reporting period before removing the float. Price that calendar time honestly rather than promising a quick change.
saying these in an interview costs you the question
- Argues the type choice purely as accuracy with no owner named
- Assumes every currency has exactly two decimal places
- Emits amounts as JSON numbers without asking who parses them
- Rounds on output while keeping a full-precision value authoritative
- Treats a later representation change as a simple refactor
- Reaches for an arbitrary-precision type without stating the rounding rule