Skip to content

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.

  1. Currency Safety: Can’t accidentally add USD and EUR
  2. Precision: Integer storage avoids floating-point errors
  3. Validation: Built-in currency validation on input
  4. Aggregation: SUM, AVG with currency mismatch detection
DuckDB

          

In double-entry bookkeeping, every transaction must balance - debits equal credits.

DuckDB

          

A key invariant: all entries for a transaction must sum to zero.

DuckDB

          

Calculate current balance for each account:

DuckDB

          

Handle multiple currencies with proper isolation:

DuckDB

          

When converting between currencies, use a currency exchange account:

-- Add exchange account
INSERT 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');

Generate a trial balance showing all accounts:

DuckDB

          

Ledgers should be append-only. Use soft deletes via reversal entries:

-- Instead of deleting entry 1, create a reversal
INSERT INTO ledger_entries VALUES
(9, 'REV001', 'cash', '-100 USD', 'Reversal of TXN001'),
(10, 'REV001', 'revenue', '100 USD', 'Reversal of TXN001');
  1. Index account_id: Most queries filter by account
  2. Partition by date: For large ledgers, partition by month/year
  3. Materialized balances: Cache current balances for frequently accessed accounts
  4. Batch inserts: Use transactions for multi-entry operations
-- Useful indexes
CREATE 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);
Featurepgledgermonetary + custom
DatabasePostgreSQL onlyDuckDB + PostgreSQL
Currency handlingString-basedType-safe monetary
Multi-currencyManual validationBuilt-in currency checks
Exchange ratesNot included10 built-in sources
ConstraintsSQL checksType system enforced

The monetary extension provides the currency safety layer, while you build the ledger logic on top.