AkshadipsinhBackend · Distributed Systems
All projects
02Apr. 2026 – Jul. 2026Completed

AtomicX

Financial Ledger API

An ACID-compliant double-entry ledger REST API that transfers funds with exactly-once semantics, staying correct under hundreds of concurrent threads and deadlock-free by construction.

Java 21Spring BootPostgreSQLREST API
ACCOUNT AsenderACCOUNT BreceivertransferLOCKCHECKACTDEBIT−100.0000 · account ACREDIT+100.0000 · account Bone @Transactional(timeout=10) boundary
1,000
RPS at 13 ms average transfer latency
1,600
RPS sustained under k6 load testing
27
unit tests: concurrency, precision, idempotency
500+
concurrent threads handled
01

Overview

AtomicX implements canonical double-entry bookkeeping behind a REST API. Every transfer is a single database transaction that writes one transaction record and exactly two ledger entries — one DEBIT, one CREDIT — with four-decimal BigDecimal precision throughout.

02

The problem

Money movement is the classic case where a small inconsistency compounds: a lost update quietly changes a balance, a deadlock stalls the service, and a retried request can move funds twice.

AtomicX is deliberately correctness-first. It spends a little extra latency per transfer to guarantee that the double-entry invariant — the sum of debits equals the sum of credits — always holds.

03

Architecture

Transfer pipeline
Transaction manager
Persistence
Immutable ledger

Transfer pipeline

Request arrives → idempotency lookup → fail-fast limit check → lock accounts in UUID order → check balance from the ledger → write transaction + two ledger entries, all inside one transactional boundary.

Transaction manager

@Transactional(timeout = 10) wraps the whole path, so locks are never held indefinitely and any exception releases them through an automatic rollback.

Persistence

PostgreSQL with HikariCP (50 max connections) and Flyway-managed schema migrations. Idempotency keys and account ids are indexed for O(1) lookups.

Immutable ledger

Ledger entries are append-only: no update or delete paths exist. Balance is a SUM over the ledger, which makes the invariants observable rather than assumed.

04

Design decisions

01

Pessimistic write locks

Both participating accounts are locked with PESSIMISTIC_WRITE for the critical section. This blocks conflicting writers at the database level and gives deterministic, predictable latency instead of optimistic retry storms.

02

Deterministic UUID lock ordering

Locks are always acquired lower-UUID first. If every transfer follows the same order, a circular wait — and therefore a deadlock — becomes structurally impossible.

03

Idempotency keys

Header-supplied keys are persisted under a unique index. Duplicate requests short-circuit on an O(1) indexed lookup before any lock is taken, so a network retry can never double-spend.

04

Immutable append-only ledger

Balances are derived by summing append-only DEBIT/CREDIT entries. The ledger is the single source of truth, every mutation is auditable, and there is no separate balance row that can drift out of sync.

05

LOCK → CHECK → ACT in one transaction

Validation, locking, and the debit/credit pair all commit inside a single @Transactional(timeout = 10) boundary. Any failure rolls the whole transfer back within a ten-second guardrail.

05

Failure & concurrency

Two threads transferring between the same accounts

Pessimistic locks serialise access; the second thread waits and then reads a consistent, committed balance.

Opposite transfer directions at the same time

Deterministic UUID ordering means both threads request the same lock first, so neither can hold what the other needs — no deadlock.

Client retries a transfer after a timeout

The repeated idempotency key returns the cached transaction and creates no new ledger entries, so funds move once.

Failure mid-transfer

If anything throws before commit, the debit and credit entries are both rolled back, leaving the ledger untouched.

06

Testing & verification

  • 27 unit tests grouped by concern: concurrency, transactional integrity, BigDecimal precision, idempotency, validation, limits, and retrieval.
  • Concurrency tests drive dozens of parallel threads and assert the final balance is exactly correct.
  • Integration tests boot the full Spring context against an ephemeral PostgreSQL container to exercise real locking.

Measured on the project's own k6 load tests (Hikari pool of 50, Tomcat pool of 500). These are results from that environment, not a universal guarantee.