LedgerGuard is a real-time financial transaction reconciliation system built with Java, Spring Boot, and Kafka Streams. It correlates independently produced payment and ledger events, identifies inconsistencies, and emits an auditable outcome stream.
The project uses synthetic data and rules. It contains no employer code, proprietary calculations, or internal system details.
- Event-time stream processing and out-of-order event handling
- Stateful Kafka Streams joins
- Exactly-once processing
- Bounded, persistent event-ID deduplication
- Explicit exception routing
- Idempotent Kafka production
- Same-origin operations dashboard backed by the live event stream
- User-defined payment and ledger events with an inspectable processing trace
- Public-demo input validation, concurrency protection, and rate limiting
- Metrics with Spring Boot Actuator, Micrometer, and Prometheus
- Topology, domain, simulator, dashboard, and real-broker tests
flowchart LR
G["Browser reconciliation console"] -->|"POST scenario"| A["Demo API / scenario generator"]
A -->|"payments.v1"| B["Apache Kafka"]
A -->|"ledger-entries.v1"| B
B --> C["Kafka Streams reconciliation engine"]
C -->|"reconciliations.v1"| B
C -->|"reconciliation-exceptions.v1"| B
B --> D["In-memory demo result projection"]
D --> E["Reconciliation REST API"]
E --> G
C --> F["Actuator / Prometheus metrics"]
The system is intentionally split into three focused modules:
| Module | Responsibility |
|---|---|
ledgerguard-contracts |
Immutable payment, ledger, and reconciliation event contracts |
ledgerguard-reconciliation |
Deduplication, event-time joins, classification, routing, and metrics |
ledgerguard-demo-api |
Interactive transaction workbench, scenario generation, processing trace, and a queryable projection of recent outcomes |
| Status | Meaning |
|---|---|
MATCHED |
Amount and currency agree |
AMOUNT_MISMATCH |
Currencies agree but amounts differ |
CURRENCY_MISMATCH |
Payment and ledger currencies differ |
MISSING_LEDGER_ENTRY |
A payment has no ledger counterpart when the join window closes |
MISSING_PAYMENT |
A ledger entry has no payment counterpart when the join window closes |
DUPLICATE_PAYMENT |
A payment event ID was already observed |
DUPLICATE_LEDGER_ENTRY |
A ledger event ID was already observed |
Requirements:
- Java 21
- Docker Desktop or another Docker-compatible runtime
Build and start Kafka:
./mvnw clean verify
docker compose up -d --waitStart the reconciliation engine:
java -jar ledgerguard-reconciliation/target/ledgerguard-reconciliation-0.1.0-SNAPSHOT.jarIn another terminal, start the demo API:
java -jar ledgerguard-demo-api/target/ledgerguard-demo-api-0.1.0-SNAPSHOT.jarOpen http://localhost:8080 to use the interactive reconciliation console. You can enter payment and ledger values, remove either side, change event order and delay, duplicate a payment, and then inspect the real records and decisions at every processing stage. Seven repeatable scenarios are also included.
The REST API remains available for command-line use:
curl -X POST http://localhost:8080/api/scenarios/amount-mismatch
curl http://localhost:8080/api/reconciliationsSubmit your own event pair:
curl -X POST http://localhost:8080/api/transactions \
-H 'Content-Type: application/json' \
-d '{
"transactionId": "RECRUITER-DEMO-01",
"paymentAmount": 275.00,
"paymentCurrency": "CAD",
"ledgerAmount": 271.50,
"ledgerCurrency": "CAD",
"eventOrder": "LEDGER_FIRST",
"eventDelayMs": 500,
"duplicatePayment": false
}'Available scenarios:
matched
amount-mismatch
currency-mismatch
missing-ledger-entry
missing-payment
duplicate-payment
out-of-order-match
The engine exposes health and metrics on port 8081:
curl http://localhost:8081/actuator/health
curl http://localhost:8081/actuator/metrics/ledgerguard.reconciliations
curl http://localhost:8081/actuator/prometheusLedgerGuard uses event timestamps rather than wall-clock processing time. Payments and ledger entries are joined within a 10-second window with a 3-second grace period. This lets records reconcile even when they arrive in the opposite order.
Duplicate IDs are retained in persistent window stores for 24 hours. This bounds state growth while covering a realistic upstream retry horizon. Both decisions are documented in docs/adr.
The repository includes separate production Dockerfiles for the demo API and reconciliation engine, plus compose.deploy.yml for the complete three-service stack. Only the demo API should receive a public HTTP domain; Kafka and the stream engine communicate over the host's private network.
For Railway, point the two Java services at their matching railway.*.json files and set KAFKA_BOOTSTRAP_SERVERS to the Kafka service's private address. Both applications honor the platform-provided PORT value and expose readiness health checks.
- Domain tests exercise every reconciliation classification.
TopologyTestDrivertests the real Kafka Streams topology without mocks.- Simulator tests verify the exact event patterns produced by demo scenarios.
- Dashboard asset tests protect the browser console's critical controls and API integration points.
- A Spring context test verifies production constructor wiring.
- A Testcontainers smoke test validates compatibility with the official Apache Kafka image when Docker is available.
Run all checks with:
./mvnw verify- PostgreSQL-backed query projection and audit history
- OpenTelemetry traces across publishing and reconciliation
- Schema evolution and compatibility checks
- Controlled replay from historical offsets
MIT License. Copyright 2026 Steve Armstrong.