Mohamed AjabSoftware / security / dataLet’s talk
Back to the project mapCL / 17 / CASE STUDY
Working full-stack payment application4 min read

ClearLedger

Follow a payment from a signed-in operator to a balanced journal and an event.

ClearLedger is a Java and React application for account transfers. It validates tenant identity, handles retries, updates balances and journal entries together, and lets operators create, find and export records.

Read the case study
01 / CONTEXT

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.

02 / IMPLEMENTATION

How it works

The workflow, at a glance
  1. Idempotent payment request
  2. Balanced journal + transactional outbox
  3. 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.

03 / REASONING

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.

04 / WALKTHROUGH

A transfer whose response is lost

ILLUSTRATIVE EXAMPLE

A client submits a £25 transfer with an idempotency key and loses the connection after the database commit.

  1. The committed transaction contains the balance changes, balanced journal, payment record and outbox event.
  2. The client repeats the identical request with the same tenant and key, allowing the service to return the existing payment.
  3. 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.

05 / EVIDENCE

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.

06 / BOUNDARIES

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.

Where the work could go next

Possible next steps—not features or experiments already completed.

  1. Add a durable consumer inbox and reconciliation checks for repeated events and balance discrepancies.
  2. Add journal-to-balance reconciliation and crash-injection tests around outbox acknowledgement and publication marking.
07 / REFERENCE

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.