What this project is about
A payment request can fail at an awkward point: the database may commit even though the caller never receives the response. Retrying then risks moving money twice. ClearLedger explores this problem through account transfers, a double-entry journal and a React operations console. Its purpose is to make consistency and recovery decisions visible in a runnable backend project.
The implementation combines Java 21, Spring Boot, PostgreSQL and a Kafka-compatible broker. A modular monolith keeps related business operations in one application while separating HTTP, payment, account, ledger and publishing packages. This is a portfolio engineering demonstrator with a local container environment; the repository does not establish production adoption, payment-processing certification or independently assessed security.
How it works
- Idempotent payment request
- Balanced journal + transactional outbox
- Operations UI + event delivery
One transaction for the monetary change
Amounts use integer minor units, so £25 is represented as 2,500 rather than a floating-point approximation. The payment service locks the two accounts in a consistent identifier order, checks ownership, currency and funds, and updates both balances. The payment record, journal postings and pending event are written in the same PostgreSQL transaction. PostgreSQL checks that postings balance before a payment is marked POSTED.
Retry handling and event publication
An idempotency key identifies a logical payment across repeated requests. ClearLedger scopes that key to a tenant and compares the request payload, returning the existing payment for a matching retry and rejecting conflicting reuse. A transactional outbox stores pending messages alongside the payment. A separate publisher sends them to the broker and records success after acknowledgement, leaving unpublished work available for a later attempt.
An inspectable operational surface
The React and TypeScript console supports real sign-in, payment creation, account search and journal exports. Keycloak supplies identity tokens through a PKCE sign-in flow. The API verifies issuer, expiry and audience, derives tenant identity from the token, and applies operator or reader permissions. Tenant headers cannot switch organisations. Local container profiles also use authentication; a separately selected in-memory interface demo remains explicitly labelled.
The important design choices
Keep the consistency boundary together
Use one PostgreSQL transaction rather than separating balances, payments and journals into independently deployed services.
The trade-offAtomic changes are easier to reason about, but payment writes remain coupled to the primary database. Separating services later would require a new consistency design, not simply moving packages.
Make message duplication explicit
Publish committed outbox events asynchronously with an at-least-once delivery contract.
The trade-offA crash after broker acknowledgement can cause a duplicate publication. Consumers need durable deduplication by event ID, and operators must monitor publication delay as well as queue size.
A transfer whose response is lost
A client submits a £25 transfer with an idempotency key and loses the connection after the database commit.
- The committed transaction contains the balance changes, balanced journal, payment record and outbox event.
- The client repeats the identical request with the same tenant and key, allowing the service to return the existing payment.
- The publisher delivers the pending event; a downstream consumer uses its stable ID to recognise any repeated publication.
What this showsThe design distinguishes a retried payment instruction from a possibly repeated event delivery. Consumer deduplication is a required integration responsibility.
Illustrative example, not a production incident.
How it was tested—and what it shows
The checks
Repository tests cover account and journal rules, payment-service behavior and package boundaries. The PostgreSQL integration test applies real migrations and exercises repeated payment requests; it is skipped when Docker is unavailable. Frontend checks include static analysis, TypeScript compilation and a production build. These are useful implementation checks, but they do not establish throughput under contention or recovery across every process-crash boundary.
What to take from the results
The project connects database consistency, authenticated tenant boundaries and recoverable asynchronous work. Tenant identity now comes from verified credentials. Reconciliation, consumer deduplication, rate limiting and measured contention behaviour remain production-hardening work. Dashboard sample totals are not actual processed payment volume.
What to keep in mind
- This is a portfolio application, not a certified payment service or evidence of real transaction volume.
- At-least-once event delivery can repeat messages; downstream consumers still need deduplication.
- Automated reconciliation, a consumer inbox and production rate limiting remain further work.
Technical notes & source
Built with
Java 21 · Spring Boot 4 · PostgreSQL 17 · Redpanda · React 19
Terms used in this project
- Idempotency key
- An identifier that lets a repeated request return the original result instead of creating another payment.
- Transactional outbox
- Pending messages saved in the same transaction as the business change, then delivered afterwards.
- Tenant
- An organisation whose accounts and records must stay separate from other organisations.