Ledger Service is a double-entry ledger API for moving money between accounts. Every entry has to balance, history can never be rewritten, and the interesting part is keeping all of that true when lots of requests hit the same accounts at once.

Not spending the same balance twice

Every write goes through a single path in one database transaction. Before checking a balance, the accounts that lose money are locked, always in the same order (by id), so two transfers can't both see the same balance and concurrent transfers never deadlock.

  • Idempotency keys make a retried request return the original response instead of creating a second payment.
  • Amounts are BigDecimal with an explicit currency, and travel as strings so no client parses them as floats.
  • Errors follow RFC 9457 Problem Details, and the API is documented with OpenAPI and Swagger UI.

Immutable history and Kafka events

Mistakes are fixed with reversal entries, never by editing old ones. Besides the rules in the Java code, PostgreSQL triggers reject any UPDATE, DELETE or TRUNCATE on the history. Each entry is published to Kafka with the transactional outbox pattern. The event is saved in the same transaction as the entry and a separate relay sends it after the commit, so no event is lost or sent for something that never happened.

Testing and load

There are 74 tests running against real PostgreSQL and Kafka with Testcontainers. One of them fires 20 simultaneous transfers of 10.00 from an account holding 100.00 and checks that exactly 10 succeed. Load testing with k6 on my laptop, the API handles 500 transfers per second with no errors and a p95 under 35 ms.

  • A single very busy account tops out at about 120 transfers per second because of its row lock. That's a deliberate trade-off.
  • Next, I'd store the balance in a column to raise that limit and make the outbox relay faster.