Double-Entry Ledgers
Double-entry bookkeeping is a foundational accounting system where every transaction affects at least two accounts - one debit and one credit - ensuring the books always balance. The monetary extension provides the perfect foundation for building ledger systems with type-safe currency handling.
This guide is inspired by pgledger, a PostgreSQL double-entry ledger implementation by Paul Gross.
Why Use monetary for Ledgers?
Section titled “Why Use monetary for Ledgers?”- Currency Safety: Can’t accidentally add USD and EUR
- Precision: Integer storage avoids floating-point errors
- Validation: Built-in currency validation on input
- Aggregation: SUM, AVG with currency mismatch detection
Basic Ledger Schema
Section titled “Basic Ledger Schema”Recording Transactions
Section titled “Recording Transactions”In double-entry bookkeeping, every transaction must balance - debits equal credits.
Validating Balance
Section titled “Validating Balance”A key invariant: all entries for a transaction must sum to zero.
Account Balances
Section titled “Account Balances”Calculate current balance for each account:
Multi-Currency Ledger
Section titled “Multi-Currency Ledger”Handle multiple currencies with proper isolation:
Currency Conversion Transactions
Section titled “Currency Conversion Transactions”When converting between currencies, use a currency exchange account:
-- Add exchange accountINSERT INTO accounts VALUES ('fx_gains', 'FX Gains/Losses', 'revenue', 'USD', true);
-- Convert 100 EUR to USD at 1.085 rate-- This requires 4 entries to maintain balance in both currencies:-- 1. Reduce EUR cash-- 2. Close EUR to exchange account-- 3. Open USD from exchange account-- 4. Increase USD cash
INSERT INTO ledger_entries VALUES -- EUR side (7, 'FX001', 'cash_eur', '-100 EUR', 'EUR converted'), -- USD side (100 EUR * 1.085 = 108.50 USD) (8, 'FX001', 'cash', '108.50 USD', 'USD received from FX');Trial Balance Report
Section titled “Trial Balance Report”Generate a trial balance showing all accounts:
Immutability Pattern
Section titled “Immutability Pattern”Ledgers should be append-only. Use soft deletes via reversal entries:
-- Instead of deleting entry 1, create a reversalINSERT INTO ledger_entries VALUES (9, 'REV001', 'cash', '-100 USD', 'Reversal of TXN001'), (10, 'REV001', 'revenue', '100 USD', 'Reversal of TXN001');Performance Tips
Section titled “Performance Tips”- Index account_id: Most queries filter by account
- Partition by date: For large ledgers, partition by month/year
- Materialized balances: Cache current balances for frequently accessed accounts
- Batch inserts: Use transactions for multi-entry operations
-- Useful indexesCREATE INDEX idx_entries_account ON ledger_entries(account_id);CREATE INDEX idx_entries_txn ON ledger_entries(transaction_id);CREATE INDEX idx_entries_date ON ledger_entries(created_at);Comparison with pgledger
Section titled “Comparison with pgledger”| Feature | pgledger | monetary + custom |
|---|---|---|
| Database | PostgreSQL only | DuckDB + PostgreSQL |
| Currency handling | String-based | Type-safe monetary |
| Multi-currency | Manual validation | Built-in currency checks |
| Exchange rates | Not included | 10 built-in sources |
| Constraints | SQL checks | Type system enforced |
The monetary extension provides the currency safety layer, while you build the ledger logic on top.
Further Reading
Section titled “Further Reading”- pgledger on GitHub - PostgreSQL ledger implementation
- Double-Entry Ledgers: The Missing Primitive - Paul Gross’s blog
- Building a Financial Ledger - Performance considerations