An append-only, double-entry ledger with idempotent writes in Java 21 — the core of how a payment system records money correctly. No runtime dependencies; JUnit 5 for tests.
The same pattern I keep in Go (go version), implemented in idiomatic modern Java (records, sealed-style reason enums,
switchexpressions). Clean-room and original.
- Append-only — entries are never mutated; every balance reconstructs from the log, so history is fully auditable.
- Double-entry — each transfer writes a matching debit and credit, so all
balances always sum to zero.
audit()proves it. - Idempotent — each write carries a key. Replaying it (retried request,
redelivered message) applies the money once; reusing a key for a
different transfer throws
IDEMPOTENCY_CONFLICT.
Money is stored as long minor units. Never double for money. The class
is thread-safe.
var ledger = new Ledger();
ledger.deposit("seed-1", "alice", 1_000, "funding"); // External -> alice
ledger.transfer("invoice-77", "alice", "bob", 250, "rent"); // alice -> bob
// A retried request with the same key applies once:
var retry = ledger.transfer("invoice-77", "alice", "bob", 250, "rent");
retry.replayed(); // true — no double charge
ledger.balance("bob"); // 250
ledger.audit(); // throws IllegalStateException if ever inconsistent| Call | Result |
|---|---|
| New key | applied; replayed() == false |
| Same key, same params | cached result; replayed() == true; applied once |
| Same key, different params | LedgerException(IDEMPOTENCY_CONFLICT); nothing applied |
mvn testNine JUnit 5 tests cover deposits/transfers, conservation, idempotent replay, idempotency conflict, insufficient funds, validation, append-only monotonicity, and two concurrency tests — 500 distinct transfers across a thread pool, and 100 threads racing a single idempotency key (which must apply exactly once).
Requires JDK 21+ and Maven.
Replace the in-memory List/Map with a write-ahead log or an append to a
Kafka topic and project balances from it. The idempotency and double-entry
logic — the part that's easy to get wrong — stays identical.
MIT