Original credit-transfer payments microservice by Md Tanvir Alam (tanvir-ux).
demo of how a high-concurrency bank-style initiation API is usually shaped: JWT resource server, idempotent POST, JPA persistence, a sanctions stub, and Kafka domain events. It is not employer IP and is not affiliated with any core-banking vendor or commercial bank. Message field names are ISO 20022 / SWIFT-inspired (public standards: debtor/creditor parties, instructed amount + currency).
Stack: Spring Boot 3.3 / Java 17, PostgreSQL, Kafka (KRaft), OpenAPI, Actuator.
flowchart LR
Client[Client / recruiter curl] -->|Bearer JWT + Idempotency-Key| API[PaymentController]
API --> SVC[PaymentService]
SVC --> AML[AmlScreeningService stub]
SVC --> DB[(PostgreSQL payments)]
SVC -->|payment.initiated / payment.posted| K[Kafka]
API -.->|OpenAPI| Swagger[swagger-ui]
API -.->|health / info| Actuator
Happy path inside one transaction (demo simplification of a real posting engine):
sequenceDiagram
participant C as Client
participant API as REST
participant S as PaymentService
participant AML as AML stub
participant DB as Postgres
participant K as Kafka
C->>API: POST /api/v1/payments
API->>S: create(request, Idempotency-Key)
S->>DB: persist INITIATED
S->>K: payment.initiated
S->>AML: screen creditor name
alt name contains SANCTIONED_DEMO
S->>DB: REJECTED
API-->>C: 201 Payment status REJECTED
else clear
S->>DB: VALIDATED then POSTED
S->>K: payment.posted
API-->>C: 201 Payment status POSTED
end
| Demo choice | What production initiation APIs typically do |
|---|---|
Idempotency-Key + SHA-256 of canonical payload |
Exactly-once acceptance under client retries / at-least-once HTTP. Unique index + hash compare (409 on mismatch). |
| Unique constraint race handling | Two concurrent POSTs with the same key: one insert wins, the other maps DataIntegrityViolation back to the stored row. |
| JWT resource server (HS256 demo secret) | OAuth2/OIDC from an IdP; scopes per product (payments:write). HS256 is demo only. |
| Synchronous INITIATED → POSTED | Real systems accept quickly (INITIATED/ACCEPTED), then post asynchronously on a ledger/clearing worker; clients poll or consume status events. |
Kafka payment.initiated / payment.posted |
Downstream AML, notifications, liquidity, and general-ledger consumers. Topics stay schema-stable (ISO 20022-inspired). |
Pagination on GET /payments |
Ops/investigation screens; never dump the ledger. |
| Actuator health | K8s probes; readiness should eventually include Postgres + Kafka. |
docker compose up --buildServices:
- API: http://localhost:8080
- Swagger UI: http://localhost:8080/swagger-ui.html
- OpenAPI JSON: http://localhost:8080/v3/api-docs
- Health: http://localhost:8080/actuator/health
- Postgres:
localhost:5432(user/password/dbpayments) - Kafka (KRaft, no ZooKeeper):
kafka:9092on the compose network
Mint a demo JWT (the token endpoint is unauthenticated on purpose for local recruiter runs):
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/demo/token | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")Create a payment:
curl -s -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen || cat /proc/sys/kernel/random/uuid)" \
-H "Content-Type: application/json" \
-d '{
"debtor": { "name": "Ada Lovelace", "account": "NL91ABNA0417164300" },
"creditor": { "name": "Grace Hopper", "account": "DE89370400440532013000" },
"amount": 100.00,
"currency": "EUR"
}'Replay the same key + body: HTTP 200, same id. Change the amount, keep the key: HTTP 409.
Sanctions stub (beneficiary name contains SANCTIONED_DEMO):
curl -s -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen || cat /proc/sys/kernel/random/uuid)" \
-H "Content-Type: application/json" \
-d '{
"debtor": { "name": "Ada Lovelace", "account": "NL91ABNA0417164300" },
"creditor": { "name": "SANCTIONED_DEMO Holdings", "account": "DE89370400440532013000" },
"amount": 1.00,
"currency": "EUR"
}'Fetch / list:
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/payments/{id}
curl -s -H "Authorization: Bearer $TOKEN" "http://localhost:8080/api/v1/payments?page=0&size=20&sort=createdAt,desc"Offline token (same demo secret as application.yml):
python3 scripts/mint-demo-jwt.pyapplication.yml documents:
app.security.jwt.secret: demo-hs256-secret-do-not-use-in-production-min-32b!!
app.security.jwt.issuer: payments-demoNever ship this secret to a real environment. Replace with an RSA/JWKS resource server bound to your IdP.
mvn -q test- Unit:
PaymentServiceTest,AmlScreeningServiceTest(Mockito, no Spring) - Slice/API:
PaymentControllerTest(MockMvc + H2 + real JWT encoder)
Tests disable Kafka (app.kafka.enabled=false) and use H2.
com.tanviralam.payments
api REST, DTOs, errors, demo token
application use-cases, AML stub, hashing
domain Payment aggregate + status
infrastructure JPA, OpenAPI
kafka producer + no-op for tests
security OAuth2 resource server (HS256)
MIT © 2026 Md Tanvir Alam