PostgreSQL over SQL Server for Ledger-Core Immutability Engine
- ledger-core
- postgresql
- database
- immutability
Context
Ledger-Core needs a durable, append-only store for the double-entry ledger with transactional guarantees, optimistic concurrency control, and audit replay. SQL Server was already licensed in the org, but the immutability engine runs on Linux containers in Azure Container Apps and must support fine-grained access to the write-ahead log for reconciliation tooling.
Decision
Adopt PostgreSQL 16 as the Ledger-Core immutable store. Rows are never updated or deleted after settlement; mutations append versioned records, and the write path is wrapped in serializable transactions so reconciliation can replay the ledger from a single snapshot.
Consequences — Positive
- Append-only patterns map directly to native MVCC and WAL-based replication
- Fully open-source and license-free inside container deployments
- JSONB allows schema-flexible audit metadata without migrations
Consequences — Negative
- No built-in temporal query syntax; replay logic must be application-owned
- SQL Server migration tooling for legacy ledgers must be custom-written
- Team familiarity skews toward SQL Server; ramp-up cost on PG tuning
Why not SQL Server
SQL Server is a strong operational database, but the Ledger-Core constraints worked against it: the engine must run on Linux containers without a licensing surface, the append-only write pattern maps poorly to row-level locking and page-fill habits, and the reconciliation pipeline needs raw visibility into transaction and WAL semantics. The legacy-license argument lost to the container-first deployment posture.
The immutable write path
Rows are versioned, never overwritten. A check constraint guards the version sequence so a concurrent writer cannot silently clobber a settled entry:
CREATE TABLE ledger_entries (
id BIGSERIAL PRIMARY KEY,
version INT NOT NULL CHECK (version > 0),
account_id UUID NOT NULL,
amount NUMERIC(19, 4) NOT NULL,
settled_at TIMESTAMPTZ,
audit_json JSONB,
UNIQUE (account_id, version)
);
Replay and audit
Because the store is append-only, the auditor's view is a pure function of the WAL and the versioned table — no temporal extensions, no triggers, no shadow tables. The cost is that all replay logic is application-owned: the serializable-transaction write path is the sole writer, and anything that bypasses it breaks the audit chain.