Ledger Base
A backend wallet service covering idempotency, concurrency, and race-condition handling — built to work through the failure modes that only bite you in production.
Role
Stack
Source
The problem
Financial-style backends have a class of bugs that only bite you in production — duplicated charges from retried requests, concurrent writes corrupting a balance, two simultaneous debits both reading the same snapshot before either commits. I built this to work through those problems deliberately, with the constraints of a real system in mind.
There was no third-party payment processor to offload correctness to. Every operation had to be idempotent, transactionally safe, and race-condition-free from first principles.
Key decisions
Idempotency keys on every write
Client retries — from network failures, timeouts, or a user double-tapping — should never produce a duplicate operation. Each write carries a client-generated key; the server checks it before executing and returns the cached result if it has seen that key before.
The tradeoff
Extra storage and a lookup on every write request. Worth it: a duplicated debit is far worse than a few milliseconds of overhead.
Row-level locking over optimistic concurrency
Optimistic concurrency — read, modify, write back, retry on conflict — adds retry logic and unpredictable latency under contention. For balance updates I wanted correctness I could reason about on a whiteboard, so SELECT … FOR UPDATE takes the row lock and the second writer waits.
The tradeoff
More contention under very high write volume — requests queue behind the lock. At the scale this system targets, that is the right trade.
The hard part
The nastiest bug was a race where two concurrent debits both read the same balance before either wrote back.
Balance: ₦1,000
Request A reads ₦1,000 → deducts ₦600
Request B reads ₦1,000 → deducts ₦600
Balance ends at ₦400 — but ₦1,200 was debited.
The fix was wrapping the read-modify-write in a transaction with a row-level lock, so only one operation can hold the row at a time. Everything else in the debit path stayed the same — the correctness came from four words in the right place.
BEGIN;SELECT balanceFROM walletsWHERE id = $1FOR UPDATE;UPDATE walletsSET balance = balance - $2WHERE id = $1;INSERT INTO transactions (wallet_id, amount, type, idempotency_key)VALUES ($1, $2, 'debit', $3);COMMIT;
Where it landed
Credit, debit and transfer are all idempotent, transactionally safe, and correct under concurrent writes. It shipped as a personal project — the goal was to understand the patterns behind financial backends deeply enough to build them without a processor holding the correctness for me. I'm considering hosting it publicly as a reference implementation.
What I took from it
Idempotency is not an optimisation. It is a correctness requirement for any system that accepts retries.
Pessimistic locking is underrated. Optimistic concurrency is elegant right up until you are debugging a retry storm at 2 AM.
Writing the race condition out explicitly — in a test, in a comment, on paper — is the fastest way to understand it well enough to fix it.