The half everybody already knows
Prices are not floating-point numbers. Store the amount as an integer in the currency's minor unit, carry the currency code beside it, and never let a binary fraction near a number a customer will read. Nobody argues with this once it is said out loud. Call that the easy half: a schema decision, one helper, and a few weeks of review comments. Nothing here disputes it, and nothing here is about it. The expensive half is that rounding is not a step. It is a set of sites, and the question that decides whether your prices are correct is how many of them a single amount passes through on its way to a screen.
Markup per room, or markup on the total
A concrete fork, from hotel offers. Two supplier patterns are documented on our side: in one, the supplier returns a single token that books N rooms and quotes one price for the lot. In the other, each room's rate is independent, and the offer we display is a materialised combination — its price is, structurally, a sum of independent room rates.
A tenant's markup can be applied in two defensible places. Apply it to each room's rate, round each result, then sum. Or sum the raw rates, apply the markup, round once. Both are reasonable, they do not produce the same number, and neither is a bug you can point at.
Per booking the gap is a rounding unit, sometimes a few — invisible in review. Across a month it has no bound: it grows with volume and does not average out, because half-up rounding is not symmetric around a sum. The visible version is worse than the accounting one. A traveller reads a per-room breakdown, adds the lines, and gets a different number than the total underneath them — which is not a rounding conversation with a finance team but a trust conversation with support.
A rounding rule chosen once, and written down
The failure I actually care about is not choosing the wrong rule. It is not
choosing. Nobody writes down "half-up, to the currency's minor unit" — it
simply happens, inherited from whichever formatter ran last: a toFixed in a
UI component, a template helper, a column with a fixed scale, or a supplier
response that arrived already rounded to its own convention before we ever saw
it. Every one of those is a monetary decision taken by someone who was not
thinking about money at the time. And the moment a currency with no minor
digits, or three of them, enters the tenant list, the inherited default is
wrong somewhere and stays wrong quietly.
So the rule gets stated once — mode and precision, per currency — in one function, and the set of places allowed to call it stays short enough to list.
What is displayed and what is charged
The second discipline: display rounding is a projection, and a projection never flows back. The number the customer is charged is the stored integer. The bug shape to watch for is a rounded display value read back in as an input to arithmetic: at that moment the presentation layer has become a participant in pricing, and nothing downstream can tell.
Conversion is a third site, and it has a timestamp
Suppliers quote in their currency; a tenant sells in another. Conversion rounds, which makes it the third site. It also uses a rate, and a rate has a time. Store the converted amount without the rate and the timestamp that produced it and recomputing that booking later reprices it — the same booking answering differently on two days, with no way to tell a bug from the market. So the converted amount is stored as a fact alongside its rate and timestamp, not as a derivation waiting to be re-derived. What the customer pays and what is owed to the supplier are two facts in two currencies; after the sale, neither recomputes the other.
The number I do not have
There is no reconciliation figure on this page. I never ran the month-long diff of per-line markup against per-total markup, so I cannot tell you what the difference came to, in either direction. The argument is about discipline, not about a recovered amount. If someone says they fixed rounding and hands you a figure, they measured. I did not.
The rule
Money needs one representation, one rounding rule, and one place that applies it. Another lesson on this site is about three services disagreeing on one price. This one is smaller and more embarrassing: one service disagreeing with itself, because the same amount took two different routes through rounding and both routes were written by people who were right.