beginner 3 min answer

An order total is stored as the integer 1000 alongside a currency code. In USD that is ten dollars. The same integer means one thousand yen in JPY and one dinar in KWD. A report sums the column across currencies and the total looks plausible. Why is this a reference-data problem rather than a bug in the report?

reference datacurrencyminor unitsiso 4217bitemporal
Show the full answer Hide the answer

The mechanism

Storing money as an integer count of minor units is the correct choice: it avoids floating-point rounding and makes arithmetic exact. What makes it work is the minor-unit exponent, the number of decimal places a currency has, and that exponent is a property of the currency rather than of the row. ISO 4217 publishes it. It is 2 for USD and EUR, 0 for JPY, and 3 for KWD, BHD and JOD.

The bug is not in the report. The bug is that the exponent was written into application code as the constant 2, so the currency table is incomplete and the report has no way to be right. Any report, any model and any export built on that column will make the same error, which is the definition of a reference-data problem: one missing attribute of a shared vocabulary produces wrong answers in every system that consumes it.

The consequence people miss

An FX conversion step does not rescue it and usually hides it. Converting 1000 JPY minor units at the yen rate while assuming two decimals understates that order by a factor of 100, and the result is a plausible number in the right ballpark for a small order. Nothing errors, no row is rejected, and the discrepancy surfaces as a reconciliation break months later with no obvious cause.

The same class of mistake with a different shape: KWD at three decimals overstates by a factor of ten in the opposite direction, so a mixed-currency total can look close to correct while every individual line is wrong.

Why the exponent has to be dated

Minor-unit rules are stable but not permanent. Currencies are redenominated: the Turkish lira redenomination in 2005 removed six zeros from the unit. A row recorded before a redenomination must resolve its exponent as of the transaction date, not as of today, which is why the currency reference table needs valid-from and valid-to columns and a join on the transaction date rather than a lookup of the current row. Overwriting the row in place silently rewrites history.

Where the distinction stops mattering

In a single-currency product, none of this is visible and hard-coding the exponent costs nothing. The moment a second currency is accepted, or a marketplace admits a seller in another country, the exponent must come from a versioned reference table. That transition is the cheapest it will ever be on the day the second currency is added and the most expensive after two years of history.

Common weak answers

  • "Store a decimal type instead." A decimal removes the binary rounding problem and tells you nothing about how many places this currency has, how to round a division, or how to display it. You still need the reference table.
  • "Store the formatted string." Display is now correct and arithmetic is gone. Every consumer parses it back, each with its own assumptions.
  • "The report should group by currency." That hides the error rather than fixing it, and the next consumer, who does not group, repeats it.
  • "Convert everything to a base currency on write." Then the stored amount depends on the rate at write time, and restating a prior period becomes impossible.