---
title: Implementing Robust and Accurate Money Handling in Payment Systems: Why Floating-Point Numbers Fail and Integers Prevail for Precision
created: 2026-03-04
updated: 2026-03-04
type: summary
tags: ["money", "notes", "payment", "precision", "telegram"]
external: http://go/ui/posts/2112/
---

# Handling Money & Cents in a Payment System

This is a classic and critical topic. The short answer is: **never use floating-point types for money**. Here's the full explanation:

---

## The Problem with Floats

Floating-point numbers (`float`, `double`) use **binary representation (IEEE 754)**, which **cannot accurately represent many decimal fractions**:

```python
# Python example
>>> 0.1 + 0.2
0.30000000000000004

>>> 0.1 + 0.2 == 0.3
False
```

```javascript
// JavaScript example
> 0.1 + 0.2
0.30000000000000004

> 19.99 * 100
1998.9999999999998
```

In a payment system, these tiny errors **accumulate** and lead to:
- Customers being overcharged or undercharged by fractions of a cent
- Ledger entries that don't balance
- Reconciliation failures against bank statements
- Tax calculation errors
- Legal and compliance issues

> 💸 **A rounding error of $0.01 per transaction × 10 million transactions/day = $100,000/day lost or gained incorrectly.**

---

## The Solution: Use the Smallest Currency Unit as an Integer

Store all monetary values as **integers in the smallest unit of the currency** (e.g., cents for USD, pence for GBP):

| Display Value | Stored Value | Currency | Smallest Unit |
|---|---|---|---|
| $19.99 | `1999` | USD | cent |
| £9.50 | `950` | GBP | penny |
| ¥1000 | `1000` | JPY | yen (no subunit) |
| BHD 5.123 | `5123` | BHD | fils (3 decimal places) |

### Example Schema

```sql name=schema.sql
CREATE TABLE payments (
    id              BIGINT PRIMARY KEY,
    amount          BIGINT NOT NULL,          -- in smallest currency unit (e.g., cents)
    currency        VARCHAR(3) NOT NULL,      -- ISO 4217 code: "USD", "EUR", "JPY"
    status          VARCHAR(20) NOT NULL,
    created_at      TIMESTAMP NOT NULL
);

-- $19.99 USD is stored as:
-- amount = 1999, currency = 'USD'
```

### Example in Code

```java name=Money.java
// Java — use long for integer cents
public class Money {
    private final long amountInSmallestUnit;  // e.g., cents
    private final Currency currency;          // ISO 4217

    public Money(long amountInSmallestUnit, Currency currency) {
        this.amountInSmallestUnit = amountInSmallestUnit;
        this.currency = currency;
    }

    // $19.99 → new Money(1999, Currency.USD)

    public String toDisplayString() {
        int decimalPlaces = currency.getDefaultFractionDigits(); // 2 for USD, 0 for JPY
        double displayValue = amountInSmallestUnit / Math.pow(10, decimalPlaces);
        return String.format("%,."+decimalPlaces+"f %s", displayValue, currency.getCurrencyCode());
    }
}
```

```python name=money.py
# Python
from dataclasses import dataclass

@dataclass
class Money:
    amount: int        # smallest unit (e.g., cents)
    currency: str      # ISO 4217 code

    def to_display(self) -> str:
        decimal_places = CURRENCY_DECIMALS.get(self.currency, 2)
        display_value = self.amount / (10 ** decimal_places)
        return f"{display_value:.{decimal_places}f} {self.currency}"

# Currency decimal places (ISO 4217)
CURRENCY_DECIMALS = {
    "USD": 2,  # $1.00   = 100 cents
    "EUR": 2,  # €1.00   = 100 cents
    "GBP": 2,  # £1.00   = 100 pence
    "JPY": 0,  # ¥1      = 1 yen (no subunit)
    "BHD": 3,  # 1.000   = 1000 fils
    "KWD": 3,  # 1.000   = 1000 fils
}

# $19.99 → Money(amount=1999, currency="USD")
# ¥1000  → Money(amount=1000, currency="JPY")
```

---

## What If You Truly Need Decimal Precision?

In some cases (e.g., exchange rate calculations, tax computations, financial reporting), you may need **exact decimal arithmetic**. The answer is **NOT floats** — use language-specific **decimal types**:

| Language | Type to Use | ❌ Never Use |
|---|---|---|
| Java | `BigDecimal` | `float`, `double` |
| Python | `decimal.Decimal` | `float` |
| C# | `decimal` (128-bit) | `float`, `double` |
| Go | `shopspring/decimal` or `int64` cents | `float32`, `float64` |
| JavaScript | Libraries like `dinero.js`, `big.js` | `Number` (all JS numbers are IEEE 754 doubles) |
| Ruby | `BigDecimal` | `Float` |
| PostgreSQL | `NUMERIC` / `DECIMAL` | `REAL`, `DOUBLE PRECISION` |
| MySQL | `DECIMAL(19,4)` | `FLOAT`, `DOUBLE` |

### Example: Using BigDecimal in Java

```java name=TaxCalculation.java
import java.math.BigDecimal;
import java.math.RoundingMode;

// Tax calculation example
BigDecimal price = new BigDecimal("19.99");
BigDecimal taxRate = new BigDecimal("0.0825");  // 8.25% tax

BigDecimal tax = price.multiply(taxRate).setScale(2, RoundingMode.HALF_UP);
// tax = 1.65 (exact)

BigDecimal total = price.add(tax);
// total = 21.64 (exact)
```

### Example: Using Decimal in Python

```python name=tax_calculation.py
from decimal import Decimal, ROUND_HALF_UP

price = Decimal("19.99")
tax_rate = Decimal("0.0825")

tax = (price * tax_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
# tax = Decimal('1.65')

total = price + tax
# total = Decimal('21.64')
```

---

## Industry Standard: How Real Companies Do It

| Company | Approach |
|---|---|
| **Stripe** | All amounts in smallest currency unit as integers. `amount: 1999` for $19.99 |
| **PayPal** | Integer cents internally |
| **Square** | Integer cents |
| **Adyen** | Integer in minor units |
| **Shopify** | Integer cents with `money_format` for display |

This is not a coincidence — it's the universally accepted practice.

---

## Watch Out: Multi-Currency Gotcha

Not all currencies have 2 decimal places:

| Decimal Places | Currencies | Smallest Unit |
|---|---|---|
| **0** | JPY, KRW, VND | 1 yen/won/dong |
| **2** | USD, EUR, GBP, AUD (most currencies) | 1 cent/penny |
| **3** | BHD, KWD, OMR | 1 fils/baisa |

Your system **must** know the decimal places per currency (defined by **ISO 4217**) to correctly convert between display values and stored integers.

---

## Summary of Rules

| ✅ Do | ❌ Don't |
|---|---|
| Store money as integers in the smallest currency unit | Use `float` or `double` for money |
| Use `BigDecimal` / `Decimal` for intermediate calculations | Use string-to-float parsing for monetary values |
| Always pair amount with a currency code (ISO 4217) | Assume all currencies have 2 decimal places |
| Convert to display format only at the **presentation layer** | Store pre-formatted strings like `"$19.99"` |
| Round explicitly with a defined rounding mode | Let the language silently round for you |
| Use well-tested money libraries (`dinero.js`, `Joda-Money`, `py-moneyed`) | Roll your own money arithmetic from scratch |

**The golden rule:** Money enters your system as an integer in the smallest unit, stays as an integer through all storage and transfer, and only becomes a decimal for human-readable display at the very last moment.

---

Would you like me to dive into any specific aspect, such as handling currency conversion, rounding strategies for tax calculations, or designing the money type for a specific language?
