Currency Code Best Practices in Database Design
Best practices for storing currency codes and monetary amounts in databases. Covers column types, indexing, CHAR(3) vs enums, minor unit storage, and schema design patterns.
Detailed Explanation
Storing Currency Data in Databases
How you store currency codes and monetary amounts in your database has long-term implications for correctness, performance, and flexibility. Here are proven best practices.
Currency Code Column
-- RECOMMENDED: CHAR(3) for ISO 4217 codes
currency CHAR(3) NOT NULL DEFAULT 'USD'
-- Also acceptable: VARCHAR(3) if your DB handles it efficiently
currency VARCHAR(3) NOT NULL DEFAULT 'USD'
-- NOT recommended: ENUM (inflexible, hard to add new currencies)
currency ENUM('USD', 'EUR', 'GBP') -- Adding a currency requires ALTER TABLE
Why CHAR(3)?
- ISO 4217 codes are always exactly 3 characters
- Fixed-width is more efficient for indexing and comparison
- No need for the flexibility of VARCHAR
Monetary Amount Column
-- RECOMMENDED: Store amounts in minor units as integers
amount BIGINT NOT NULL -- Cents, yen, fils, etc.
-- Also acceptable: DECIMAL for human-readable storage
amount DECIMAL(19, 4) NOT NULL -- Supports up to 15 integer + 4 decimal digits
-- NOT recommended: FLOAT or DOUBLE
amount FLOAT -- Precision loss! 0.1 + 0.2 != 0.3
Complete Schema Example
CREATE TABLE transactions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
amount BIGINT NOT NULL, -- Minor units
currency CHAR(3) NOT NULL, -- ISO 4217
amount_decimal DECIMAL(19, 4) GENERATED ALWAYS AS (
amount / POWER(10, CASE currency
WHEN 'JPY' THEN 0
WHEN 'KWD' THEN 3
WHEN 'BHD' THEN 3
ELSE 2
END)
) VIRTUAL, -- Computed column for display
exchange_rate DECIMAL(18, 8), -- Rate at time of transaction
base_amount BIGINT, -- Equivalent in base currency
base_currency CHAR(3) DEFAULT 'USD',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_transactions_currency ON transactions(currency);
PostgreSQL Money Type?
PostgreSQL has a built-in money type, but it is not recommended:
- Tied to the database locale setting
- No built-in multi-currency support
- Precision is fixed (2 decimals) — fails for JPY, KWD
- Use
BIGINTorNUMERICinstead
Key Principles
- Never use floating-point for monetary amounts
- Always store the currency code alongside the amount
- Use integers (minor units) when possible for simplicity and correctness
- Index the currency column if you query by currency frequently
- Store the exchange rate used at the time of any conversion
- Use CHAR(3) for currency codes, not TEXT or ENUM
- Add CHECK constraints to validate currency codes if your DB supports it
Migration Considerations
When adding multi-currency support to an existing single-currency system:
-- Step 1: Add currency column with default
ALTER TABLE orders ADD COLUMN currency CHAR(3) NOT NULL DEFAULT 'USD';
-- Step 2: Convert existing amounts to minor units (if stored as decimals)
UPDATE orders SET amount_cents = ROUND(amount * 100);
-- Step 3: Add new minor-unit column, then swap
ALTER TABLE orders ADD COLUMN amount_minor BIGINT;
UPDATE orders SET amount_minor = ROUND(amount * 100);
Use Case
Database architects and backend developers designing schemas for financial applications, e-commerce platforms, or any system handling multiple currencies need to make these decisions early. Changing from FLOAT to BIGINT or adding a currency column to an existing table is costly in production. Getting the schema right from the start saves significant migration effort.