Dummy payment aggregator with three separate NestJS microservices:
Merchant
|
v
payment-orchestrator :3000
| Kafka: payment.attempt.events, payment.completed
| RabbitMQ: verification + webhook dispatch
v
metrics-service :3001 experiment-service :3002
| Redis hot metrics | assignment, outcomes, bandit state
v v
Redis + PostgreSQL + Kafka + RabbitMQ
services/payment-orchestrator: payment API, routing, gateway simulation, circuit breaker, outbox, verifier, webhooks.services/metrics-service: Kafka attempt consumer, sliding-window Redis metrics, PostgreSQL metric history.services/experiment-service: experiments, deterministic assignment, bandit updates, experiment outcomes, analyzer worker.
# Payment Orchestrator health
GET /ops/liveness service liveness
GET /ops/readiness DB and Redis readiness
# Payments
POST /payments create a payment
GET /payments/{paymentId} get payment details
GET /payments/{paymentId}/attempts list gateway attempts
GET /payments/{paymentId}/routing-decision
get stored routing decision
GET /payments/{paymentId}/status-verification
get async verification status
# Gateway admin
GET /gateways list gateway configs
GET /gateways/{gatewayName} get one gateway config
PATCH /gateways/{gatewayName} update gateway simulator config
# Scoring and routing debug
GET /scoring/debug inspect ranked gateways for a payment context
# Circuit breaker
GET /circuit-breaker/status list circuit breaker states
POST /circuit-breaker/force-open/{gatewayName}/{method}
force a route open/unhealthy
POST /circuit-breaker/force-close/{gatewayName}/{method}
force a route closed/healthy
# Retry policies
GET /retry-policy list retry policies
GET /retry-policy/{merchantId} get merchant retry policy
PUT /retry-policy/{merchantId} create/update merchant retry policy
# Metrics Service health
GET :3001/ops/liveness service liveness
GET :3001/ops/readiness DB and Redis readiness
# Metrics
GET :3001/metrics/health Kafka consumer and buffer status
GET :3001/metrics/gateways all gateway hot metrics
GET :3001/metrics/gateways/{gatewayName} gateway metrics grouped by method
GET :3001/metrics/gateways/{gatewayName}/{method}
gateway-method hot metrics
GET :3001/metrics/history/{gatewayName} persisted metric history
# Experiment Service health
GET :3002/ops/liveness service liveness
GET :3002/ops/readiness DB and Redis readiness
# Experiments
POST :3002/experiments create experiment
GET :3002/experiments list experiments
GET :3002/experiments/assign assign customer to active experiment
GET :3002/experiments/{experimentId} get experiment
PATCH :3002/experiments/{experimentId} update experiment
POST :3002/experiments/{experimentId}/pause
pause experiment
POST :3002/experiments/{experimentId}/complete
complete experiment
GET :3002/experiments/{experimentId}/results
get experiment results
# Bandit
GET :3002/bandit/state/{segment} get Thompson Sampling state
GET :3002/bandit/order/{segment} sample gateway order
POST :3002/bandit/reset/{segment}/{gatewayName}
reset gateway bandit state
docker compose up -d --build
docker compose exec -T payment-orchestrator node dist/database/seeds/seed.jsYou can also run this application, without sourcecode, all service images are uploaded on docker-hub and can run with a single docker-compose file.
docker compose -f docker-compose.dockerhub.yml up -dHealth checks:
curl http://localhost:3000/ops/readiness
curl http://localhost:3001/ops/readiness
curl http://localhost:3002/ops/readinessUseful UIs:
- Kafka UI:
http://localhost:8080 - RabbitMQ Management:
http://localhost:15672(guest/guest)
Create UPI payment:
curl -X POST http://localhost:3000/payments \
-H "Content-Type: application/json" \
-H "idempotency-key: pay-upi-001" \
-d '{
"merchantId": "amazon",
"customerId": "cust_001",
"amountMinor": 33000,
"paymentMethod": "UPI",
"instrument": { "vpa": "user@paytm", "bank": "HDFC" }
}'Create card payment:
curl -X POST http://localhost:3000/payments \
-H "Content-Type: application/json" \
-H "idempotency-key: pay-card-001" \
-d '{
"merchantId": "swiggy",
"customerId": "cust_002",
"amountMinor": 150000,
"paymentMethod": "CARD",
"instrument": { "cardNetwork": "VISA", "bank": "HDFC", "last4": "1234" }
}'Payment lookups:
curl http://localhost:3000/payments/{paymentId}
curl http://localhost:3000/payments/{paymentId}/attempts
curl http://localhost:3000/payments/{paymentId}/routing-decision
curl http://localhost:3000/payments/{paymentId}/status-verificationGateway routes:
curl http://localhost:3000/gateways
curl http://localhost:3000/gateways/razorpay
curl -X PATCH http://localhost:3000/gateways/razorpay \
-H "Content-Type: application/json" \
-H "X-Internal-Api-Key: dev-internal-key-change-in-prod" \
-d '{ "successRate": 0.10 }'Scoring debug:
curl "http://localhost:3000/scoring/debug?method=UPI&bank=HDFC&amountMinor=33000"Circuit breaker:
curl http://localhost:3000/circuit-breaker/status
curl -X POST http://localhost:3000/circuit-breaker/force-open/razorpay/UPI \
-H "X-Internal-Api-Key: dev-internal-key-change-in-prod"
curl -X POST http://localhost:3000/circuit-breaker/force-close/razorpay/UPI \
-H "X-Internal-Api-Key: dev-internal-key-change-in-prod"Retry policy:
curl -H "X-Internal-Api-Key: dev-internal-key-change-in-prod" \
http://localhost:3000/retry-policy
curl -X PUT http://localhost:3000/retry-policy/amazon \
-H "Content-Type: application/json" \
-H "X-Internal-Api-Key: dev-internal-key-change-in-prod" \
-d '{ "maxAttempts": 5, "latencyBudgetMs": 15000, "retryOnTimeout": false }'curl http://localhost:3001/metrics/health
curl http://localhost:3001/metrics/gateways
curl http://localhost:3001/metrics/gateways/cashfree
curl http://localhost:3001/metrics/gateways/cashfree/UPI
curl http://localhost:3001/metrics/history/cashfreeCreate an experiment:
curl -X POST http://localhost:3002/experiments \
-H "Content-Type: application/json" \
-H "X-Internal-Api-Key: dev-internal-key-change-in-prod" \
-d '{
"name": "Cashfree first for UPI",
"controlWeight": 0.5,
"treatmentWeight": 0.5,
"treatmentConfig": { "gatewayOrderOverride": ["cashfree","razorpay","payu","juspay","hdfc_gateway"] },
"targetMethods": ["UPI"]
}'Experiment operations:
curl http://localhost:3002/experiments
curl "http://localhost:3002/experiments/assign?customerId=cust_001&method=UPI"
curl http://localhost:3002/experiments/{experimentId}
curl http://localhost:3002/experiments/{experimentId}/results
curl -X POST http://localhost:3002/experiments/{experimentId}/pause \
-H "X-Internal-Api-Key: dev-internal-key-change-in-prod"
curl -X POST http://localhost:3002/experiments/{experimentId}/complete \
-H "X-Internal-Api-Key: dev-internal-key-change-in-prod"Bandit:
curl http://localhost:3002/bandit/state/UPI
curl "http://localhost:3002/bandit/order/UPI?gateways=cashfree,razorpay,payu,juspay"
curl -X POST http://localhost:3002/bandit/reset/UPI/cashfree \
-H "X-Internal-Api-Key: dev-internal-key-change-in-prod"POST /paymentsis throttled at 100 requests per minute per IP.- Idempotency is enforced with the
idempotency-keyheader. - Circuit breaker state and hot metrics live in Redis.
- Payment attempts and completions are published to Kafka.
- Timeout payments are resolved through RabbitMQ verification.
