A small fintech backend that moves money safely: every balance change is an
atomic, double-entry ledger posting; transfers are idempotent (retry without
double-charging) and overdraw-proof under concurrency. The money core is
Java/Spring Boot; the satellites (auth, FX, notifications, card gateway) are
NestJS. Everything runs behind Traefik with one docker compose up.
Live demo: https://payment.sugoiweb3.uk/
- Swagger (wallet-service) — https://payment.sugoiweb3.uk/swagger-ui.html
- Grafana dashboards — https://payment.sugoiweb3.uk/grafana
- Wallet Swagger — http://localhost:8180/swagger-ui.html
- User service Swagger — http://localhost:3004/api/docs
- FX service Swagger — http://localhost:3002/api/docs
- Payment gateway Swagger — http://localhost:3005/api/docs
- Notification service Swagger — http://localhost:3003/api/docs
- Grafana — http://localhost/grafana
- Traefik dashboard — http://localhost:8888
docker compose up -d --buildFor local debugging (exposes service ports directly) and dashboards:
# expose ports (psql, redis, direct service hits)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# + Grafana & Prometheus
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile observability up -dThen:
| What | Where |
|---|---|
| API docs (Swagger) | http://localhost/swagger-ui.html |
| OpenAPI JSON | http://localhost/v3/api-docs |
| Runnable requests | test/http/*.http (VS Code REST Client / IntelliJ) |
| Traefik dashboard | http://localhost:8888 |
| Grafana / Prometheus | http://localhost/grafana · http://localhost/prometheus |
A seeded admin (admin@vietpay.com / Admin123!) can fund wallets. The fastest
tour is test/http/transfer.http and test/http/payments.http — run top-to-bottom.
Single entry point (Traefik :80), path-routed to each service:
| Route | Service | Does |
|---|---|---|
/auth |
user-service (NestJS) | register / login / refresh; issues the JWT |
/api/v1/** |
wallet-service (Java) | wallets, transfers, deposits, ledger — the graded core |
/payments |
payment-gateway (NestJS) | card top-ups → drives the wallet ledger via Kafka |
/api/fx |
fx-service (NestJS) | live Vietcombank exchange rates |
/notifications |
notification-service (NestJS) | consumes events, sends receipts |
| (internal) | visa-service / mastercard-service | simulated card acquirers |
Interactive docs: http://localhost/swagger-ui.html (wallet-service, public via Traefik).
The NestJS services expose Swagger at /api/docs on their own port when started
with the dev overlay (NODE_ENV=development). Every service also has
GET /healthcheck and GET /metrics.
wallet-service — /api/v1 · Swagger http://localhost/swagger-ui.html
POST /api/v1/wallets— open a walletGET /api/v1/wallets— list my walletsGET /api/v1/wallets/{id}— get balanceGET /api/v1/wallets/{id}/transactions— ledger history (paged)GET /api/v1/wallets/{id}/reconciliation— prove balance == ledger sumPOST /api/v1/wallets/{id}/deposits— fund a wallet (ADMIN,Idempotency-Key)POST /api/v1/transfers— send money (Idempotency-Key)GET /api/v1/transfers/{id}— transfer detail (participants only)
user-service — /auth · Swagger http://localhost:3004/api/docs
POST /auth/register·POST /auth/login·POST /auth/refresh·POST /auth/logoutGET /auth/iam— current identity ·GET /auth/verify— used by Traefik ForwardAuthGET /internal/users/{id}— service-to-service (blocked at the edge)
payment-gateway — /payments · Swagger http://localhost:3005/api/docs
POST /payments/topups— card top-up (Idempotency-Key)POST /payments/webhooks/{scheme}— acquirer settlement (HMAC-signed; internal)
fx-service — /api/fx · Swagger http://localhost:3002/api/docs
GET /api/fx/rates— all ratesGET /api/fx/rates/{code}— one currencyGET /api/fx/rates/{code}/history— rate history
notification-service — /notifications · Swagger http://localhost:3003/api/docs
POST /notifications/{email|payment|transfer/credit|welcome|otp}- consumes Kafka
transfer-events/user-events;/internal/kafka/*admin
POST /acquirer/authorize·POST /acquirer/capture— called east-west by the gateway
The hard parts (idempotency + concurrency), on a real Postgres via Testcontainers:
cd wallet-service && mvn verifyIdempotencyIT— same key twice → money moves once, ledger balances.ConcurrencyIT— 20 threads on an under-funded wallet → no overdraw, ledger reconciles.PaymentConsumerIT/PaymentEventsConsumerTest— card top-up applied exactly once + DLQ.
NestJS unit tests (gateway HMAC + idempotency, acquirer BIN):
cd payment-gateway && npm test # and: cd acquirer-sim && npm testEnd-to-end (through the running stack): run test/http/transfer.http and
test/http/payments.http top-to-bottom — login → create wallet → deposit →
transfer / card top-up → check balance & history.
Load test (k6, transfers through the full edge → JWT → pessimistic-locked tx):
k6 run test/k6/transfer-load.jsDone
- scoped:
- Safe transfers + admin deposits on an immutable double-entry ledger
- DB-level idempotency (unique key + replay) and concurrency-proof overdraw prevention
- Schema migrations (Flyway / TypeORM) — no auto-create
- JWT auth at the Traefik edge (ForwardAuth)
- Card top-up — gateway → Visa/Mastercard sims → outbox→Kafka → idempotent consumer + DLQ
- FX rates, email notifications, Redis caching
- Prometheus + Grafana dashboards, structured logs, OpenTelemetry traces
- Fully Dockerised (
docker compose up)
- not in scope:
- User management (registration, login, profile updates) with greeting email
- Admin panel for managing users and transactions
Known limitations
- 1 user can have multiple wallets, that could be confusing — a real system would have a single wallet per user, or a more complex hierarchy.
- OpenTelemetry enable but not jeager-exported (no Jaeger container, no traces in Grafana)
With more time
- Cross-currency transfers via
fx-servicerates with an exchange feature - fees system
- Card payout saga (money out, with compensating reversal)
- A fraud-service with the Resilience4j circuit breaker fully wired