Represent money as an amount plus a currency, and make rounding rules part of each calculation. Store durable monetary facts in exact numeric types or carefully defined integer units. Never use binary floating point for values whose decimal result must be reproduced exactly.
Amount alone is incomplete
“100” could mean one dollar in cents, 100 Japanese yen, or 0.100 units of a three-decimal currency. Every database column, message, and API field needs a currency and an unambiguous scale. Name integer fields amount_minor, not amount, and document the currency metadata that defines the unit.
Integer minor units are compact and exact, but they are not universally two decimal places. Currency conventions change, some transactions use greater precision than cash, and prorating often creates fractions smaller than the final settlement unit. A decimal representation can preserve calculation precision, provided scale and rounding are constrained.
PostgreSQL’s numeric type documentation distinguishes exact numeric types from inexact floating-point types and explains precision and scale. Use NUMERIC for exact decimal arithmetic, set a scale appropriate to the domain, and test how the database rounds values that exceed it.
Separate facts from calculations
Store immutable inputs: unit price, currency, quantity, tax rate and jurisdiction, discount rule version, exchange-rate value and source time, and rounding policy. Store the calculated line totals and invoice total as facts once a commercial event is finalized. Recomputing an old invoice with today’s tax or exchange rate destroys historical reproducibility.
Do not update a price row in place when it has been used. Create a new version with an effective interval. An order references the precise price version it accepted.
Avoid a single money column whose currency comes from the customer profile. Accounts can change billing currency, and a record may combine settlement, display, and reporting currencies. Put currency beside every amount unless the table has an immutable currency invariant enforced by design.
Define rounding boundaries
Choose rounding mode, increment, and timing for every workflow. Rounding each line and then summing can differ from summing exact values and rounding once. Tax authorities, payment rails, and customer contracts may require a specific order.
Allocation exposes the remainder. Splitting 10.00 three ways yields 3.33, 3.33, and 3.34. Use a deterministic rule for distributing residual minor units, such as largest remainder with a stable tie-breaker, and store the allocation. Never let the missing cent disappear.
Keep calculation precision higher than settlement precision, then round at the named boundary. Reject mixed-currency addition. Currency conversion produces a new money value and an exchange-rate fact; it is not ordinary multiplication without context.
Keep formatting outside the model
Formatting is presentation, not storage. The ECMAScript Internationalization specification defines Intl.NumberFormat behavior for locale-sensitive number and currency display. Use locale and currency to render symbols, separators, and digits, but send the underlying exact amount separately.
Do not parse localized display strings back into ledger values. Symbols can be ambiguous, grouping varies, and user input needs a locale-aware parser with explicit currency confirmation.
Test monetary properties
Test zero- and three-decimal currency examples, negative refunds, very large totals, half-way rounding, allocation remainders, mixed currencies, exchange-rate versioning, and serialization across services. Check database, application language, JSON encoding, and reporting tools together.
For APIs, serialize exact amounts as strings or integers with explicit units when client number formats cannot preserve the full range. Reject unknown currency codes and excessive scale at the boundary instead of rounding silently during deserialization.
Define a Money value object that prevents arithmetic across currencies and requires an explicit rounding context for division or conversion. Then trace one invoice from catalog price through tax, payment, refund, ledger entry, and UI. Any place that drops currency, scale, or rate provenance is the next bug to remove.
Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.
Read our editorial approach ↗