Production-grade Saga Orchestration Pattern for managing distributed transactions across microservices with full compensation logic, idempotency, and observability.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API GATEWAY (Kong) β
β Rate Limiting Β· Auth Β· Request Routing β
βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SAGA ORCHESTRATOR SERVICE β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Saga State β β Step Runner β β Compensation Manager β β
β β Machine β β (Async) β β (Rollback Logic) β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββ β
β β
β State Store: Redis (distributed locks) or in-memory (PoC) β
ββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββββ ββββββββββββββββ ββββββββββββ ββββββββββββββββββ
β Order β β Payment β βInventory β β Notification β
β Service β β Service β β Service β β Service β
β β β β β β β β
β Postgres β β Postgres β β Postgres β β Email/SMS β
β (Orders) β β (Ledger) β β (Stock) β β β
ββββββββββββ ββββββββββββββββ ββββββββββββ ββββββββββββββββββ
β β β β
ββββββββββββββββ΄βββββββ¬ββββββββ΄βββββββββββββββ
β
ββββββββββββΌβββββββββββ
β Apache Kafka β
β β
β Topics: β
β β’ saga-*-commands β
β β’ saga-replies β
β β’ saga-events β
β β’ *.dlq β
ββββββββββββββββββββββ
Note: Kong is shown as an optional API gateway. It is not included in
docker-compose.yml.
Client βββΊ Orchestrator: CREATE_ORDER_SAGA
β
ββStep 1βββΊ OrderService: RESERVE_ORDER
β βββ SUCCESS: order_id=123, status=RESERVED
β
ββStep 2βββΊ PaymentService: PROCESS_PAYMENT
β βββ SUCCESS: payment_id=456, amount=99.99
β
ββStep 3βββΊ InventoryService: RESERVE_INVENTORY
β βββ FAILURE: insufficient_stock
β
β β COMPENSATION TRIGGERED (reverse order)
β
ββComp 2βββΊ PaymentService: REFUND_PAYMENT (payment_id=456)
β βββ SUCCESS: refund_id=789
β
ββComp 1βββΊ OrderService: CANCEL_ORDER (order_id=123)
β βββ SUCCESS: status=CANCELLED
β
ββββΊ Client: ORDER_FAILED (saga_id=xxx, reason=OUT_OF_STOCK)
We chose orchestration β see ADR-001 for full reasoning.
Every saga step is idempotent via idempotency keys β see ADR-004.
STARTED β RUNNING β COMPLETED
β RUNNING β COMPENSATING β FAILED
β COMPENSATION_FAILED (needs manual intervention β see runbook)
βββ saga-orchestrator/
β βββ src/
β β βββ SagaOrchestrator.ts # Core orchestration engine + saga registry
β β βββ SagaStateMachine.ts # State transitions + validation
β β βββ CompensationManager.ts # Reverse-order rollback logic
β β βββ StepExecutor.ts # Async step dispatch with retry + timeout
β β βββ SagaRepository.ts # All DB access for saga state
β β βββ saga-bootstrap.service.ts # Registers saga definitions + Kafka consumers
β β βββ db-init.service.ts # Idempotent schema bootstrap on startup
β β βββ app.module.ts # NestJS module wiring
β β βββ main.ts # NestJS bootstrap (Fastify + shutdown hooks)
β β βββ sagas/
β β β βββ order.saga.ts # OrderSaga step definitions
β β βββ controllers/
β β β βββ orders.controller.ts # POST /api/orders β start order saga
β β β βββ sagas.controller.ts # GET /api/sagas/:id β saga status
β β β βββ metrics.controller.ts # GET /metrics β Prometheus scrape endpoint
β β β βββ probes.controller.ts # GET /live GET /ready (K8s probes)
β β βββ metrics/
β β β βββ metrics.service.ts # Prometheus counters (prom-client)
β β βββ entities/
β β β βββ saga.entity.ts # TypeORM entity
β β βββ kafka/
β β β βββ kafka.service.ts # Kafka producer + consumer (DLQ + retry)
β β βββ redis/
β β βββ redis.service.ts # Distributed locks + in-memory fallback
β βββ Dockerfile
β βββ tsconfig.json
β βββ package.json
β
βββ services/
β βββ order-service/src/index.ts # RESERVE_ORDER + CANCEL_ORDER handlers
β βββ payment-service/src/index.ts # PROCESS_PAYMENT + REFUND_PAYMENT handlers
β βββ inventory-service/src/index.ts # RESERVE_INVENTORY + RELEASE_INVENTORY handlers
β βββ notification-service/src/index.ts # SAGA_COMPLETED / SAGA_FAILED email+SMS
β
βββ deploy/
β βββ helm/saga-pattern/ # Helm chart (Deployment, Service, HPA, PDB, Ingress)
β βββ kustomize/ # Kustomize base + kind overlay
β βββ kind/ # One-command local K8s cluster scripts
β β βββ create-cluster.sh
β β βββ install-bitnami-infra.sh # Kafka + Redis + Postgres via Bitnami Helm
β β βββ build-images.sh
β β βββ deploy-app-helm.sh
β β βββ port-forward-orchestrator.sh
β βββ README.md
β
βββ docker/
β βββ docker-compose.yml
β βββ prometheus.yml
β
βββ docs/
βββ saga-flow.svg
βββ adr/
β βββ 001-orchestration-vs-choreography.md
β βββ 002-redis-for-saga-state.md
β βββ 003-kafka-for-commands.md
β βββ 004-idempotency-strategy.md
βββ runbooks/
βββ compensation-failed.md
# Start everything: Kafka, Redis, Postgres x4, Jaeger, Prometheus, Grafana
docker compose -f docker/docker-compose.yml up -d --build
# Trigger a test saga
curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "cust-123",
"items": [{"productId": "a0000000-0000-0000-0000-000000000001", "qty": 2, "unitPrice": 49.99}],
"paymentMethod": "card_xxxx_4242"
}'
# Monitor saga state
curl http://localhost:3000/api/sagas/{saga_id}
# View Kafka messages
open http://localhost:8090 # Kafka UI
# View distributed traces
open http://localhost:16686 # Jaeger
# View metrics dashboard
open http://localhost:3001 # Grafana (admin / admin)SAGA_STATE_STORE=memory docker compose -f docker/docker-compose.yml up -d --buildPrereqs: docker, kubectl, helm, kind
cd deploy/kind
./create-cluster.sh # Create local K8s cluster
./install-bitnami-infra.sh # Install Kafka, Redis, Postgres via Bitnami Helm
./build-images.sh # Build Docker images
./deploy-app-helm.sh # Deploy app via Helm
./port-forward-orchestrator.sh # Expose orchestrator on localhost:8080
# Health checks
curl http://localhost:8080/live
curl http://localhost:8080/readySee deploy/README.md for Kustomize instructions and production notes.
| Tool | Purpose | Port |
|---|---|---|
| Jaeger | Distributed tracing across saga steps | :16686 |
| Prometheus | Metrics scraping (GET /metrics) |
:9090 |
| Grafana | Metrics dashboard | :3001 |
| Kafka UI | Message inspection and replay | :8090 |
| Metric | Type | Description |
|---|---|---|
saga_started_total |
Counter | Sagas started, labeled by sagaName |
saga_completed_total |
Counter | Sagas completed successfully, labeled by sagaName |
saga_failed_total |
Counter | Sagas failed after compensation, labeled by sagaName |
| Node.js defaults | Various | CPU, memory, GC, event loop (via prom-client) |
- Distributed Locking β Redis
SET NX PXprevents duplicate step execution across pods - Idempotency Keys β each step and compensation is safe to retry
- Retry with Exponential Backoff β Kafka consumer retries with jitter before routing to DLQ
- Dead Letter Queue β unprocessable messages sent to
{topic}.dlqafter max retries - Step Timeout β Redis TTL-based timeout per step
COMPENSATION_FAILEDstate β on-call alerting for stuck compensations (see runbook)- Graceful shutdown β
enableShutdownHooks()for clean Kubernetes pod termination - Readiness probe β
/readychecks Postgres, Kafka, and Redis before accepting traffic
cd saga-orchestrator
npm run test:unit # State machine logic, compensation ordering
npm run test:integration # With real Kafka + Postgres
npm run test:chaos # Kill services mid-saga
npm run test:load # Concurrent sagas (requires Artillery)