Skip to content
Engineering

How to Store Money in Code and Databases: Minor Units, Rounding and Currencies

Floating-point numbers can't represent most money amounts exactly, and payment bugs follow. Why integers in minor units (paise, cents) are the standard, how many decimals each currency really has, how to split and round without losing a paisa, how to convert currencies, and how to format amounts for Indian and international customers.

LILPE Editorial Team9 October 2026 8 min read
Cover for a developer guide to storing money amounts with minor units and correct rounding

Ask a payment API to charge 1999 and it will charge ₹19.99, not ₹1,999. That convention, amounts as whole numbers of the smallest currency unit, catches out almost every developer once. It exists for a good reason: the obvious way to store money in most programming languages, a floating-point number, can't represent most decimal amounts exactly, and payment systems can't tolerate "almost".

This guide covers how to represent money correctly from the API to the database: the problem with floats, integer minor units, how many decimal places each currency really has, rounding and splitting amounts so nothing is lost, converting currencies, and formatting amounts for display.

Why floating point and money don't mix

Floating-point numbers store values in binary. Just as one third can't be written exactly in decimal, one tenth can't be written exactly in binary. The error is tiny, but it's there, and it surfaces in comparisons, totals and rounding:

JavaScriptResults from any JavaScript console
0.1 + 0.2            // 0.30000000000000004
0.1 + 0.2 === 0.3    // false
1.15 * 100           // 114.99999999999999  (so Math.floor gives 114, not 115)

The last line is the classic payments bug: converting ₹1.15 to paise by multiplying by 100 and truncating produces 114 paise. Add enough of these and reconciliation stops matching. The same happens with floats in Python, Java, Go and most other languages, and with FLOAT or DOUBLE columns in databases. PostgreSQL's own documentation says floating-point numbers shouldn't be used to handle money because of rounding errors.

The options, compared

There are three sound ways to hold money, and one to avoid:

Ways to represent money amounts
RepresentationExample for ₹19.99Verdict
Integer minor units1999 (paise), stored as a 64-bit integerThe standard for payment APIs and ledgers
Exact decimal type19.99 in a DECIMAL or NUMERIC column, or a decimal libraryGood, especially where you need fractions of a minor unit, such as rates
String in an API"19.99" parsed into a decimal typeFine at boundaries if parsed strictly; never compute with strings
Floating point19.99 as a float or doubleAvoid for money

Most payment APIs take amounts as integers in the smallest unit, paise for rupees; LILPE's unified API page describes amounts the same way. Inside your own system, integers in minor units are the simplest choice for stored amounts, with a decimal type for anything that needs more precision, like exchange rates or percentage fees before rounding.

How many decimal places does a currency have?

Not every currency has two. The ISO 4217 standard, maintained by SIX on behalf of ISO, publishes each currency's "minor unit": the number of decimal places used. Don't hard-code 100:

Minor units for selected currencies (ISO 4217 list published 17 September 2026)
CurrencyCodeMinor unitAn amount of 1999 means
Indian rupeeINR219.99
US dollarUSD219.99
EuroEUR219.99
Japanese yenJPY01,999
Kuwaiti dinarKWD31.999
Bahraini dinarBHD31.999

Keep a currency table from the ISO list in your code, keyed by the three-letter code. Some payment providers use their own decimal conventions for a few currencies, so check each provider's documentation where it differs, and convert at the connector rather than throughout your system.

Always store the currency next to the amount. An amount without a currency is a bug waiting for your first international customer.

Integer size

Use a 64-bit integer (BIGINT in SQL) for minor units. A 32-bit integer tops out at a little over 2.1 billion, which is about ₹2.1 crore in paise: too small for a large payment, let alone a daily total.

JavaScript needs care. Its standard Number type represents integers exactly only up to 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER). That's ample for individual payments in paise, but sums over large volumes, or currencies with three decimals and big amounts, can get close. Use BigInt for aggregates, or keep arithmetic in the database, and consider sending large values as strings in JSON.

Rounding: decide the rule, then apply it once

Fees, taxes, commissions, discounts, interest and currency conversion all produce fractions of a paisa. You have to round, and two decisions matter:

  • The rounding mode. Half-up (0.5 rounds away from zero) is what most people expect; half-even, or banker's rounding (0.5 rounds to the nearest even digit), avoids a small upward bias over many operations. Your finance and tax teams should choose, because invoices, tax and statements must agree.
  • Where you round. Rounding each line and then summing can give a different total from summing and then rounding once. Decide which your documents use, and do it the same way everywhere.
PythonExact decimal arithmetic with an explicit rounding mode
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN

fee_paise = Decimal("1999") * Decimal("0.0175")   # Decimal('34.9825')
fee_paise.quantize(Decimal("1"), rounding=ROUND_HALF_UP)       # Decimal('35')

Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP)   # Decimal('3')
Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_EVEN) # Decimal('2')

Note the string arguments: Decimal("0.0175") is exact, while Decimal(0.0175) would faithfully copy the float's binary error into your decimal.

Splitting amounts without losing a paisa

Splitting ₹10.00 three ways can't give three equal parts in paise: 333 + 333 + 333 is 999. Somebody gets the extra paisa, and your code has to decide who, or the parts won't add back to the whole. The same applies to splitting a payment between sellers, spreading a discount across items, or dividing an amount into instalments.

TypeScriptAllocate a positive amount by integer weights so the parts always sum to the total
function allocate(totalMinor: bigint, weights: number[]): bigint[] {
  const totalWeight = BigInt(weights.reduce((a, b) => a + b, 0));
  // Each part rounded down; BigInt division truncates.
  const parts = weights.map((w) => (totalMinor * BigInt(w)) / totalWeight);
  let remainder = totalMinor - parts.reduce((a, b) => a + b, 0n);
  // Hand out the leftover minor units one at a time, starting with the first part.
  for (let i = 0; remainder > 0n; i = (i + 1) % parts.length) {
    parts[i] += 1n;
    remainder -= 1n;
  }
  return parts;
}

allocate(1000n, [1, 1, 1]); // [334n, 333n, 333n]  (₹10.00 split three ways)
allocate(1999n, [70, 30]);  // [1400n, 599n]       (70/30 split of ₹19.99)

This version handles positive totals; allocate refunds by their absolute value and apply the sign afterwards. Whatever method you use, the test is simple: the parts must always add up to the total.

Converting between currencies

Currency conversion is where precision rules matter most, because rates have many decimal places:

  • Store exchange rates in a decimal type with enough precision, never as floats, along with their source and timestamp.
  • Convert from minor units to minor units: amount in the source currency's minor units, times the rate, adjusted for the difference in minor-unit exponents, rounded once with your chosen mode.
  • Record the rate used on the transaction, so any amount can be recomputed and explained later.
  • Treat a conversion as a transaction between accounts in two currencies, not as a change to an existing balance. Our guide to double-entry ledgers shows the pattern.

Our guide to multi-currency payments explains where conversion happens in a payment and who sets the rate.

Formatting for display

Formatting belongs at the edge, in the interface, never in stored data. Let the platform do it rather than concatenating symbols and commas:

JavaScriptFormatting with the built-in Intl API
const inr = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" });
inr.format(1999 / 100);    // "₹19.99"
inr.format(100000);        // "₹1,00,000.00"  (Indian lakh grouping)

Dividing by 100 here is acceptable because it's for display only and the result is never stored or used in arithmetic; for currencies with other minor units, divide by the right power of ten from your currency table. Intl applies each locale's grouping, which is why Indian customers see lakh and crore grouping with the en-IN locale.

Designing amounts in an API

If you design a payments API, or an internal service that handles money, settle these once and document them:

  • Amounts are integers in the currency's minor units, with an ISO 4217 currency code on every amount.
  • Reject decimal points in amount fields rather than guessing what was meant.
  • Validate ranges: no zero or negative amounts where they make no sense, and a sensible maximum per currency.
  • Return amounts the same way you accept them, including in webhooks and reports.
  • State the rounding rule for anything your API calculates, such as fees or taxes.

A checklist

Before shipping anything that handles money:

  • No floats or doubles anywhere money is stored, sent or calculated.
  • Amounts in minor units as 64-bit integers, or in an exact decimal type, always with a currency code.
  • Minor units per currency from the ISO 4217 list, not a hard-coded 100.
  • A documented rounding mode, applied at a documented point.
  • Splits that always sum to the original total, with tests.
  • Exchange rates stored as decimals with source and time, and recorded on each conversion.
  • Formatting only in the interface, using locale-aware formatting.

Frequently asked questions

What is the best way to store money in a database?

As an integer number of minor units (paise, cents) in a 64-bit integer column, with the currency code in another column, or in an exact DECIMAL or NUMERIC column. Never use FLOAT, DOUBLE or REAL for money.

Why do payment APIs use amounts in paise or cents?

Because whole numbers are exact. Representing ₹19.99 as 1999 paise avoids the rounding errors of floating-point numbers and makes it unambiguous how much is being charged.

Why shouldn't I use float for money?

Floating-point numbers are stored in binary and can't represent most decimal amounts exactly, so values like 0.1 carry tiny errors that show up in comparisons, totals and conversions, such as 1.15 times 100 coming out as 114.99999999999999.

Do all currencies have two decimal places?

No. ISO 4217 lists the minor unit for each currency: INR, USD and EUR use two, the Japanese yen uses none, and the Kuwaiti and Bahraini dinars use three. Look the value up rather than assuming 100.

How should I round amounts in payments?

Choose a rounding mode with your finance team, such as half-up or half-even, decide whether you round per line or on the total, and apply that rule consistently. When splitting an amount, distribute the remainder so the parts always add up to the original.

Official sources

Last reviewed . Examples, amounts and screens marked illustrative are not LILPE figures.

Route every payment to where it succeeds.

Tell us which providers you use today and where payments get stuck. We'll show you what the setup would look like on LILPE.