Architecting an Immutable Double-Entry Accounting Engine in PostgreSQL
Enforcing debit/credit balance invariants at the database engine level with trigger-based cryptographic audit hashing.
Throughput
14,200 txn/sec
Single primary node NVMe storage
Reconciliation Error
0.00%
Across 100M+ journal postings
Audit Proof
Cryptographic
SHA-256 Merkle block validation
01The Engineering Bottleneck
Most off-the-shelf business applications maintain a single "balance" column on account tables and update it via `UPDATE accounts SET balance = balance + 500 WHERE id = 12`. Under high concurrency or microservice latency spikes, this design suffers from catastrophic race conditions, phantom writes, lack of auditability during statutory tax audits, and irrecoverable ledger drift.
02Architectural Design & Invariants
In enterprise-grade ERP engineering, balances are never stored as mutable numbers. Instead, we implement an append-only, double-entry journal. Every financial movement generates an immutable journal entry consisting of at least two balanced postings where SUM(amount) MUST equal exactly zero. Balances are derived materialized views or window-aggregated ledger sums.
03Production Implementation Blueprint
sql-- Strict immutable ledger schema
CREATE TABLE journal_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
posted_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
reference_id TEXT NOT NULL,
memo TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'posted' CHECK (status IN ('posted', 'reversed')),
hash BYTEA NOT NULL,
previous_hash BYTEA NOT NULL
);
CREATE TABLE journal_postings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
journal_entry_id UUID NOT NULL REFERENCES journal_entries(id) ON DELETE RESTRICT,
account_id UUID NOT NULL REFERENCES chart_of_accounts(id),
amount NUMERIC(18, 4) NOT NULL CHECK (amount != 0), -- positive = debit, negative = credit
currency CHAR(3) NOT NULL DEFAULT 'USD'
);
-- Deferred constraint checking to guarantee atomic zero-sum balancing
CREATE OR REPLACE FUNCTION check_journal_zero_sum()
RETURNS TRIGGER AS $$
DECLARE
v_sum NUMERIC(18, 4);
BEGIN
SELECT COALESCE(SUM(amount), 0)
INTO v_sum
FROM journal_postings
WHERE journal_entry_id = NEW.journal_entry_id;
IF v_sum != 0 THEN
RAISE EXCEPTION 'Double-entry violation: Journal entry % postings do not sum to 0.0000 (Current sum: %)',
NEW.journal_entry_id, v_sum
USING ERRCODE = 'data_exception';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER trigger_enforce_zero_sum
AFTER INSERT OR UPDATE ON journal_postings
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION check_journal_zero_sum();04Architectural Invariants & Rules of Thumb
- Never use FLOAT or DOUBLE for monetary values; always enforce NUMERIC(18,4) or integer cents to avoid IEEE-754 precision rot.
- Use DEFERRABLE INITIALLY DEFERRED triggers to allow atomic insertion of multiple debits and credits within a single SQL transaction.
- Prevent record deletion with PostgreSQL REVOKE DELETE and row-level write-once rules.
- Hash each entry with the previous transaction hash (Merkle chain) to guarantee tamper-proof audit trails for external auditors.
Facing similar architecture bottlenecks in your business?
We design and implement custom ERPs, high-throughput databases, and air-gapped private AI systems tailored for high-concurrency enterprise workloads.