Nihada Handa
Business Impact //
- Guaranteed transactional integrity during simultaneous coin gifting
- Royalties settled accurately at high billing volume
- Zero double-spend or corrupted balances under concurrent load
A bank-grade Double-Entry Ledger Engine for an educational platform that mints virtual gold coins, pays dynamic author royalties, and settles high-volume billing — modeled in Next.js 16 and PostgreSQL with optimistic locking to eliminate race conditions.
The Problem
The platform needed a single source of truth for money movement across three interacting systems: learners buying virtual gold coins with real currency, authors earning dynamic royalties on every purchase, and a high-volume billing pipeline that could spike during course launches.
- Dual-currency accounting — real fiat in, virtual gold coins out.
- Dynamic author royalties recomputed per transaction.
- High-volume billing spikes during enrollment windows.
- Simultaneous coin gifting that could double-spend without guards.
The Solution
We modeled a proper Double-Entry Ledger Engine instead of a single mutable balance column. Every coin mint, gift, and royalty payout becomes an immutable journal entry — a debit on one account balanced by a credit on another — so the books are always provably balanced. Race conditions during simultaneous gifting are neutralized with optimistic locking: each account row carries a monotonically increasing version, and conflicting writes fail fast and retry rather than silently corrupting balances.
- Immutable journal entries — every movement is a debit/credit pair.
- Optimistic locking via a `version` column + conditional UPDATE.
- Idempotent transaction groups to survive retries and partial failures.
- Row-level balance derived from ledger aggregates, never trusted directly.
Architecture
Next.js 16 App Router
Server Actions + route handlers for ledger mutations.
PostgreSQL
ACID transactions with row-level locking and constraints.
Double-Entry Core
accounts → transactions → ledger_entries with invariant checks.
Optimistic Locking
version column + `UPDATE … WHERE version = $n`.
Implementation
CREATE TABLE accounts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL,
kind text NOT NULL CHECK (kind IN ('user','author','house','escrow')),
currency text NOT NULL CHECK (currency IN ('fiat','gold')),
balance numeric(18, 2) NOT NULL DEFAULT 0,
version bigint NOT NULL DEFAULT 0, -- optimistic lock
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE transactions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
idempotency text NOT NULL UNIQUE, -- safe retry key
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE ledger_entries (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
transaction_id uuid NOT NULL REFERENCES transactions(id),
account_id uuid NOT NULL REFERENCES accounts(id),
direction text NOT NULL CHECK (direction IN ('debit','credit')),
amount numeric(18, 2) NOT NULL CHECK (amount > 0),
created_at timestamptz NOT NULL DEFAULT now()
);-- Read current state, then update only if nobody else has.
SELECT balance, version FROM accounts WHERE id = $1;
UPDATE accounts
SET balance = balance + $2,
version = version + 1
WHERE id = $1
AND version = $3;
-- 0 rows affected -> a concurrent write happened -> retry the whole
-- transaction group (gift, royalty, mint) from the top.Double-Entry Ledger — core tables
| Table | Purpose | Key columns |
|---|---|---|
| accounts | Owner, currency, balance and lock version. | id · owner_id · kind · currency · balance · version |
| transactions | Idempotent grouping root for a money movement. | id · idempotency · status |
| ledger_entries | Immutable debit/credit legs that always balance. | id · transaction_id · account_id · direction · amount |