LedgerFlow is a production-minded transaction-processing platform built to demonstrate reliable asynchronous backend design with Java 21, Spring Boot, Kafka, PostgreSQL, Redis, and OpenTelemetry.
transaction-api: accepts idempotent transaction requests, owns authoritative state, records status history, operates its publication backlog, and queues stale transactions for reconciliation.transaction-processor: consumes processing and reconciliation requests, protects provider calls with retries and a circuit breaker, and emits processing/result events.payment-provider-simulator: deterministic idempotent stand-in for an unreliable downstream provider.transaction-contracts: versioned event and provider DTOs shared during the first development phase.ledgerflow-e2e-tests: boots the three applications against ephemeral PostgreSQL, Kafka, Redis, and Jaeger containers and verifies the complete lifecycle, failure handling, reconciliation, caching, and cross-service trace propagation.docs: technical specification, architecture, and ADRs.
flowchart TD
Client["Client"] --> API["Transaction API"]
API --> Database[("PostgreSQL")]
API -. "cache-aside reads" .-> Redis[("Redis")]
Database --> Publisher["Outbox Publisher"]
Publisher --> Requested["Kafka requested topic"]
Requested --> Processor["Transaction Processor"]
Processor --> Provider["Payment Provider Simulator"]
Processor --> Status["Kafka status topic"]
Status --> API
Failure handling is asynchronous and bounded:
flowchart TD
Attempt["Processing attempt"] --> Failure["Transient provider failure"]
Failure --> Retry["Kafka retry topic"]
Retry --> NextAttempt["Later processing attempt"]
NextAttempt --> Success["Status event"]
NextAttempt --> Exhausted["Attempts exhausted"]
Exhausted --> DLT["Dead-letter topic"]
Stuck transactions follow the same outbox and status-event boundaries:
flowchart TD
Scanner["Stale transaction scanner"] --> Outbox["Transactional outbox"]
Outbox --> Topic["Kafka reconciliation topic"]
Topic --> Processor["Transaction Processor"]
Processor --> Lookup["Provider decision lookup"]
Lookup --> Status["Kafka status topic"]
Status --> API["Transaction API"]
Trace export is vendor-neutral and asynchronous:
flowchart LR
API["Transaction API"] --> Collector["OpenTelemetry Collector"]
Processor["Transaction Processor"] --> Collector
Provider["Provider Simulator"] --> Collector
Collector --> Jaeger["Jaeger"]
PostgreSQL is the source of record. Redis accelerates repeated transaction-status reads but is never consulted for creation, idempotency, state transitions, or event publication. Cache failures fall back to PostgreSQL.
The transactional outbox prevents lost events across the database/Kafka boundary, while idempotency, retry topics, circuit breaking, and dead-letter handling make at-least-once processing safe.
W3C trace context is persisted with each outbox event so delayed Kafka publication remains connected to the originating HTTP trace.
- Java 21
- Maven 3.9+
- Docker with Compose
docker compose up --buildCreate a transaction:
curl -i http://localhost:8080/transactions \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: abc-123' \
-d '{"accountId":"acct-42","amount":125.50,"currency":"USD","type":"PAYMENT"}'The API returns 202 Accepted and a transaction ID. Retrieve it with:
curl http://localhost:8080/transactions/{transactionId}Health endpoints are available at /actuator/health on ports 8080, 8081, and 8082.
Open Jaeger at localhost:16686, select transaction-api, and run a search to inspect a transaction across the API, Kafka, processor, and provider. The response’s X-Trace-Id header can be pasted into Jaeger’s trace-ID search.
Operational metrics are available through the transaction API's Actuator metrics and Prometheus endpoints:
ledgerflow.outbox.pendingledgerflow.outbox.oldest.pending.ageledgerflow.outbox.publish.successledgerflow.outbox.publish.failureledgerflow.outbox.cleanup.deletedledgerflow.outbox.metrics.refresh.failureledgerflow.reconciliation.scanledgerflow.reconciliation.requestedledgerflow.reconciliation.resolvedledgerflow.reconciliation.unresolvedledgerflow.reconciliation.failureledgerflow.cache.transaction.hitledgerflow.cache.transaction.missledgerflow.cache.transaction.writeledgerflow.cache.transaction.evictionledgerflow.cache.transaction.failure
Published outbox events are retained for seven days by default and then removed in bounded, concurrency-safe batches. Unpublished events are never eligible for cleanup.
See the operations guide for signal interpretation and configuration.
Linux/macOS:
./mvnw clean verifyWindows PowerShell:
.\mvnw.cmd clean verifyverify requires a running Docker engine because the Failsafe integration-test phase starts isolated PostgreSQL, Kafka, Redis, and Jaeger containers. Unit tests alone can be run without Docker:
./mvnw testSee the testing guide for the end-to-end topology and debugging commands.
- Add Prometheus and Grafana dashboards for metrics and exemplars.
- Add Kubernetes manifests, IaC, and measured load tests.
See the technical specification for the precise contracts and invariants.