skip to content

How do you choose precision and scale for a DECIMAL money column in a multi-currency system?

level: seniorimportance: should knowfreq 33%

answer

  1. Not every currency has two decimal places
  2. Inputs and settled figures need different scales
  3. A bare number has no unit attached
  4. Think about the least valuable currency you support
  5. Decide where the rounding step happens

basics

~20 s

Pick a scale at least as large as the most subdivided currency you support, a precision covering the largest amount in the weakest currency, and always store a currency code beside the amount, since the numeric type carries no unit.

solid answer

~50 s

Three decisions, in order. **Scale**: currencies differ in minor units — some have two decimal places, some have none, some have three — so a single column-wide scale must be at least the largest you support, and unit prices, rates and interest usually need more decimals than stored totals. **Precision**: size for the largest amount in the least valuable currency you handle, where a modest sum runs to many digits, plus headroom; a common convention is a wide column such as `DECIMAL(19,4)`. **Unit**: `DECIMAL` stores a bare number, so 100 is meaningless alone — store an ISO 4217 code in a `CHAR(3)` column beside it and never sum across currencies without converting. Beyond the DDL, decide where rounding happens: keep extra scale through a calculation chain and `CAST` once, at the point the business says the figure becomes payable, rather than rounding at each step.

code

sql · 11 lines
sql
CREATE TABLE payment (
  payment_id    BIGINT        NOT NULL,
  currency_code CHAR(3)       NOT NULL,   -- ISO 4217: the unit
  amount        DECIMAL(19,4) NOT NULL,   -- scale covers the widest minor unit
  fx_rate       DECIMAL(19,8)             -- rates need far more decimals
);

-- Totals are per currency; summing across them means nothing
SELECT currency_code, SUM(amount) AS total
FROM   payment
GROUP  BY currency_code;

go deeper

for a junior

Focus on the basics that hold everywhere: money goes in DECIMAL with a declared scale, never a floating-point type, and the amount needs a currency code stored alongside it.

for a middle

Explain why one scale must cover the most subdivided currency you support, why unit prices and rates deserve more decimals than stored totals, and what precision has to accommodate.

for a senior

Show the design in full: scale and precision chosen from the currency set, a currency column enforcing the unit, rounding as an explicit agreed step with a CAST, and a rule that no aggregate crosses currencies.

for a principal

Own it as a system-wide contract — one declared monetary shape, defined rounding points, how FX rates and their effective dates are modelled, and how the representation survives crossing service boundaries as JSON or an API field.

## Why one scale does not fit every currency The reflex answer for money is scale 2, because cents. That is a property of *some* currencies, not of money. ISO 4217 assigns each currency a number of minor units: many use two decimal places, several use none at all, and a few use three. A column declared with scale 2 cannot represent the smallest legal amount in a three-decimal currency, and it stores a misleading `.00` for a currency with no minor unit. With one shared amounts table, the column-wide scale must therefore be at least the maximum minor-unit count across every currency you support. Storing a zero-minor-unit currency in a scale-3 column is harmless — the fractional digits are simply zero — while the reverse loses money. ## Prices and rates need more scale than totals A stored total is a settled figure at the currency's minor unit. A *unit price*, a tax rate, an FX rate or a per-second charge is an input that will be multiplied and divided later, and rounding it to the settlement scale first distorts everything downstream. A unit price rounded to two decimals and then multiplied by ten thousand units is off by up to fifty units of currency. So give inputs a wider scale than outputs: `unit_price DECIMAL(19,6)` feeding `line_total DECIMAL(19,4)`, for instance. The general rule is: carry the extra digits through the calculation and round once, at the step where the business declares the number final. ## Sizing the precision Precision is driven by the largest amount you must ever hold, expressed in your *least valuable* supported currency, since the same economic value needs far more digits there. Add headroom: precision is cheap to declare and expensive to widen later, and unlike scale, exceeding it is a hard error that fails the statement. A widely used convention is a single wide monetary type such as `DECIMAL(19,4)` for every amount column in a schema — enough integer digits for very large sums, four decimals to survive most rounding needs. It is a defensible default precisely because it is uniform: every amount in the system has the same declared shape, so no conversion happens implicitly when values move between tables. Some engines ship a dedicated money type with a fixed scale of four for the same reason; it is convenient but non-portable and cannot be re-scaled. ## The number carries no unit `DECIMAL` stores a magnitude. A row saying `amount = 100` is meaningless on its own, so every amounts table needs a currency column beside it — an ISO 4217 alphabetic code fits `CHAR(3)` exactly, which is the textbook honest use of a fixed-width character type. Once currency is a column, two disciplines follow. First, no aggregate may cross currencies without an explicit conversion — a `SUM(amount)` over mixed rows produces a number that means nothing, and the fix is to group by currency or convert through a rate table with its own effective dates. Second, comparisons and thresholds are per-currency: a limit expressed as 1,000 is a different rule in each currency. ## The integer-minor-units alternative Some systems store amounts as integers of the smallest unit — `BIGINT` cents — which is exact and cheap. It works, but it moves the scale out of the schema into convention: the column shows `1999` and only surrounding code knows whether that is cents, tenths of a cent, or a currency with no minor unit at all. In a multi-currency system that convention has to vary *per row*, which is exactly where it becomes fragile. `DECIMAL` with an explicit scale keeps the fact declared where every reader and every tool can see it. ## Rounding as a declared step Because excess scale is absorbed silently rather than raising an error, an accidental narrow cast anywhere in a chain quietly drops digits. That makes rounding a design decision, not an accident: - Decide the rounding step per calculation — after tax, at the invoice line, at settlement. - Make it explicit with a `CAST` to the target precision and scale at that step. - Agree the rounding rule with the business, since half-up and half-even give different totals over many rows, and the choice can be a regulatory matter. - Be aware that whether an engine rounds or truncates excess scale is implementation-defined, so do not leave that to a silent assignment on a path where it matters. ## What to say in an interview Name the three decisions — scale from the most subdivided currency and wider still for inputs, precision from the largest amount in the weakest currency plus headroom, and a currency code stored beside every amount — then add that rounding is an explicit, agreed step rather than a by-product of assignment.

  • Why is SUM(amount) over a multi-currency amounts table a bug rather than a rounding concern?
    Because the values have different units. Adding an amount in one currency to an amount in another produces a number that corresponds to no quantity at all, and no precision or rounding choice repairs it. Either group by the currency column so each total is in one unit, or convert through a rate table with explicit effective dates before aggregating.
  • Would DECIMAL(19,4) everywhere be a reasonable convention, or is per-column sizing better?
    A single wide monetary type is a defensible default: every amount has the same declared shape, so values move between tables without implicit re-scaling and nobody has to justify a width per column. Its cost is that it stops documenting the domain, and it is usually still too narrow for unit prices and rates, which need more decimals than settled amounts.
  • How would you migrate an existing scale-2 amount column to support a three-decimal currency?
    Widening the scale is the easy half — an existing value simply gains zero digits, and no data is lost. The hard half is that any amount already written for such a currency was rounded on the way in and cannot be recovered from the column, so the migration needs a source of truth to backfill from and a check for rows whose stored figure disagrees with the original.

saying these in an interview costs you the question

  • Assumes every currency has exactly two decimal places
  • Stores an amount without a currency code beside it
  • Sums amounts across currencies and calls it a total
  • Rounds unit prices to the settlement scale before multiplying
  • Treats FLOAT as acceptable because the scale varies by currency

context