Practical guide

How to store money and prices correctly in a database

Store an amount exactly, always keep its currency explicit, and round at a predefined boundary.

20 minutes · Databases

In short

Money is not an ordinary float

Binary floating point cannot represent many decimal amounts exactly. Small errors surface in sums, taxes, and comparisons, so it is not suitable for money.

Two common safe representations are an exact decimal such as PostgreSQL NUMERIC, or an integer in the smallest used unit. In both cases, the value also needs a currency and a rounding policy.

Prepare

Decisions to make first

The database type follows business rules. Define precision, currencies, and the rounding point first.

  • Supported currencies and decimal places used for accounting, prices, and intermediate calculations.
  • A rounding rule for unit price, tax, document line, and final total.
  • A decision to store historical documents as immutable snapshots instead of recalculating them from the current catalog.
  • Maximum amount and calculation range used to choose decimal precision and scale or a BIGINT range.

Steps 1 to 3

Introduce an exact money model

Choose one representation per context and do not mix decimal strings, floats, and minor units without an explicit conversion boundary.

1. Choose NUMERIC or minor units

  1. NUMERIC(p, s) stores decimal values exactly. Choose precision and scale from the maximum amount and required intermediate calculations.
  2. A BIGINT in minor units simplifies sums and comparisons. Not every currency uses two decimal places, so never hard-code conversion as times one hundred.
  3. Do not use PostgreSQL money as a universal domain model. Its input and output depend on locale and the type does not carry a currency code.
  4. NUMERIC is often practical for catalog prices and tax calculations; integer minor amounts can suit settled payments. Document the choice.
amount NUMERIC(19, 4) NOT NULL, currency CHAR(3) NOT NULL
Official PostgreSQL numeric type documentation

2. Transfer amounts without floats

  1. Accept a decimal amount from an API or form as a validated string. Do not pass it through a PHP float before storing it in NUMERIC.
  2. Use a decimal or money library with an explicit rounding mode. The string 19.90 must remain the exact value 19.90.
  3. Keep amount and ISO currency code together in a value object. Do not allow EUR and CZK to be added without an explicit exchange rate.
  4. Do not send formatted values with commas, currency symbols, or thousands separators to the database. Format only at the output boundary.
new MoneyAmount('19.90', 'CZK')
Official PHP floating-point precision documentation

3. Define one rounding boundary

  1. Name the rounding mode and where it applies. Rounding every intermediate result and rounding only at the end produce different totals.
  2. For an invoice, store unit price, quantity, rate, tax base, tax, total, and currency as approved at issue time.
  3. Use constraints for currency format and valid range. A negative value may be correct for a credit note, so derive rules from column meaning.
  4. Share the same examples and rounding mode between backend, accounting export, and tests. A frontend preview must not be the only calculation.
rounding: half-up; scale: 2; apply: invoice line tax
Official PHP rounding documentation

Step 4

Test precision and boundaries

Use values where representation and rounding actually break, not only whole units.

  1. Add problematic tenths

    Verify the exact result of 0.10 + 0.20 and preservation of trailing scale according to the model.

    SELECT 0.10::numeric + 0.20::numeric;
  2. Test ties and negative amounts

    Add exact rounding ties, a credit note, zero, and the maximum permitted value.

    php bin/phpunit --filter Money
  3. Compare a complete document

    Lines, taxes, and total must match an approved accounting example and remain identical after a database round trip.

When it goes wrong

Common mistakes

19.90 became an imprecise float in transit

Find the float conversion in the request, DTO, or serialization. Carry decimals as strings or a money value object to the database driver.

Invoice line sums do not match the total

Components use a different boundary or mode. Define whether lines, tax, or only the total is rounded and store the approved snapshot.

An integer amount is off by one hundred for a currency

The conversion assumes two decimal places. Use metadata for the particular currency and an explicit major-to-minor boundary.

The database money type formats differently on another server

It is locale-dependent. Use NUMERIC or BIGINT with a separate currency code for a portable model.

Done

Amounts are exact and their meaning is complete.

The database now stores an exact value, its currency, and the agreed business rounding. Use the same model from input through calculation to the immutable document.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.