Author: Md Tanvir Alam (tanvir-ux)
Stack: Spring Boot 3.3 · Java 17 · Apache Kafka · PostgreSQL · Docker Compose · Kubernetes
License: MIT
Original demo of three cooperating Spring Boot services that move money through Kafka events and persist a double-entry ledger. Built to show production-shaped patterns: idempotent consumers, JWT-secured REST, transactional outbox-style publish-after-commit, Kubernetes-ready manifests.
flowchart LR
Client["Client / curl"] -->|JWT REST| TA["transfer-api"]
TA -->|publish transfer.requested| K["Apache Kafka"]
K -->|consume| LS["ledger-service"]
LS -->|JPA double-entry| PG[("PostgreSQL")]
LS -->|publish transfer.posted / transfer.failed| K
K -->|consume| TA
K -->|consume| NS["notification-service"]
NS --> PG
TA --> PG
sequenceDiagram
autonumber
participant C as Client
participant API as transfer-api
participant K as Kafka
participant L as ledger-service
participant N as notification-service
C->>API: POST /api/v1/auth/token
API-->>C: JWT
C->>API: POST /api/v1/transfers (Bearer)
API->>API: persist Transfer (PENDING)
API->>K: transfer.requested
API-->>C: 202 Accepted {transferId}
K->>L: transfer.requested
alt sufficient funds + valid accounts
L->>L: debit source, credit destination (one TX)
L->>K: transfer.posted
else insufficient funds / unknown account / duplicate
L->>K: transfer.failed
end
K->>API: posted | failed
API->>API: update Transfer status
K->>N: posted | failed
N->>N: store Notification
| Topic | Producer | Consumers | Payload |
|---|---|---|---|
transfer.requested |
transfer-api | ledger-service | TransferRequestedEvent |
transfer.posted |
ledger-service | transfer-api, notification-service | TransferPostedEvent |
transfer.failed |
ledger-service | transfer-api, notification-service | TransferFailedEvent |
Every successful transfer writes two ledger_entry rows in one database transaction:
- DEBIT
fromAccount(asset outflow) - CREDIT
toAccount(asset inflow)
Balances are updated in the same transaction. Replays are no-ops because transfer_id is unique on the ledger.
| Module | Port | Responsibility |
|---|---|---|
bank-events |
— | Shared Jackson records for Kafka payloads |
transfer-api |
8080 | JWT auth, initiate transfers, track status |
ledger-service |
8081 | Post journal entries, publish posted/failed |
notification-service |
8082 | Persist and list user notifications |
Requires Docker with Compose v2.
docker compose up --buildWait until Kafka is healthy, then:
# 1. Get a token (demo users: alice/password bob/password)
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"password"}' | jq -r .token)
# 2. Transfer 25.00 from Alice (ACC-1001) to Bob (ACC-2001)
curl -s -X POST http://localhost:8080/api/v1/transfers \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: demo-001' \
-d '{"fromAccountId":"ACC-1001","toAccountId":"ACC-2001","amount":25.00,"currency":"USD"}'
# 3. Poll status
curl -s -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/transfers/<transferId>
# 4. Notifications for Bob
curl -s http://localhost:8082/api/v1/notifications?accountId=ACC-2001Seeded accounts (see ledger-service DataSeeder):
| Account | Owner | Opening balance |
|---|---|---|
ACC-1001 |
Alice | 1,000.00 USD |
ACC-2001 |
Bob | 250.00 USD |
ACC-3001 |
Treasury | 10,000.00 USD |
export JAVA_HOME=/path/to/jdk-17
mvn -q testTests use H2 in-memory databases and mocked KafkaTemplate / listener unit tests. They do not start a Kafka broker.
Manifests live in k8s/. They assume an in-cluster Kafka bootstrap of kafka:9092 and Postgres at postgres:5432 (both included as demo Deployments — not HA).
kubectl apply -k k8s/
kubectl -n bank-demo rollout status deploy/transfer-apiProduction clusters should replace the bundled Kafka/Postgres with a managed broker (MSK, Confluent, Strimzi) and a managed database. The service Deployments only need SPRING_KAFKA_BOOTSTRAP_SERVERS and SPRING_DATASOURCE_URL.
- HMAC JWT (
HS256), 30-minute TTL, secret injected via envJWT_SECRET. - Demo users are in-memory (
alice/bob). Swap for JDBC/OAuth2 in a real environment. - Actuator is exposed on
/actuator/healthonly.
See the repository tree. Each service has its own Dockerfile (multi-stage, Temurin 17).