Saga Orquestrada em .NET 10, com Amazon SQS (via LocalStack) como transporte de mensagens e PostgreSQL como store de estado. O projeto demonstra, do zero ao código real, os principais padrões de sistemas distribuídos resilientes:
- Happy path — fluxo Order → Payment → Inventory → Shipping →
Completed - Compensação em cascata — falha em qualquer passo dispara rollback reverso automático
- Idempotência — handlers não reprocessam o mesmo comando duas vezes
- DLQ visibility — inspeção e redrive de mensagens mortas
- Traces distribuídos — OpenTelemetry propagado via SQS com W3C TraceContext
- Observabilidade LGTM — traces no Grafana/Tempo, logs no Grafana/Loki, correlacionados por TraceId
- Concorrência com pessimistic locking — SELECT FOR UPDATE vs race condition real
flowchart LR
subgraph OrderService
OS_API[POST /orders]
OS_Worker[Worker order-status-updates]
end
subgraph SagaOrchestrator
ORC[Worker reply queues]
end
subgraph Workers
PAY[PaymentService]
INV[InventoryService]
SHP[ShippingService]
end
DB[(PostgreSQL)]
OS_API -->|HTTP POST /sagas| ORC
ORC -->|payment-commands| PAY
PAY -->|payment-replies| ORC
ORC -->|inventory-commands| INV
INV -->|inventory-replies| ORC
ORC -->|shipping-commands| SHP
SHP -->|shipping-replies| ORC
ORC -->|order-status-updates| OS_Worker
ORC --- DB
OS_API --- DB
OS_Worker --- DB
| Ferramenta | Versão mínima | Verificação |
|---|---|---|
| Docker | 24+ | docker --version |
| Docker Compose | v2 (plugin) | docker compose version |
| bash | 4+ | bash --version |
| curl | qualquer | curl --version |
| jq | 1.6+ | jq --version |
Windows: use Git Bash, WSL2 ou qualquer terminal com bash nativo para os scripts de demo.
# 1. Clone o repositório
git clone https://github.com/iannkzw/saga-orchestration-dotnet-sqs.git
cd saga-orchestration-dotnet-sqs
# 2. Na primeira execução, compile as imagens antes de subir
# (evita timeout de health check durante restauração de pacotes NuGet)
docker compose build
# 3. Suba todos os serviços
docker compose up -d
# 3. Aguarde os containers ficarem healthy (~30s)
docker compose psSaída esperada de docker compose ps quando tudo está pronto:
NAME STATUS
saga-lgtm Up (healthy)
saga-otelcol Up
saga-localstack Up (healthy)
saga-postgres Up (healthy)
saga-order-service Up (healthy)
saga-orchestrator Up (healthy)
saga-payment-service Up (healthy)
saga-inventory-service Up (healthy)
saga-shipping-service Up (healthy)
Confirme individualmente com:
curl -s http://localhost:5001/health | jq .
curl -s http://localhost:5002/health | jq .# Criar um pedido
curl -s -X POST http://localhost:5001/orders \
-H "Content-Type: application/json" \
-d '{
"totalAmount": 99.90,
"items": [{"productId": "PROD-001", "quantity": 1, "unitPrice": 99.90}]
}' | jq .Resposta esperada:
{
"orderId": "3fa85f64-...",
"sagaId": "7c9e6679-..."
}Verifique o estado final da saga (aguarde ~2s para o orquestrador processar):
SAGA_ID="<sagaId da resposta acima>"
curl -s http://localhost:5002/sagas/$SAGA_ID | jq '{state: .state, transitions: [.transitions[].to]}'Resposta esperada:
{
"state": "Completed",
"transitions": [
"PaymentProcessing",
"InventoryReserving",
"ShippingScheduling",
"Completed"
]
}Verifique o status do pedido (atualizado de forma assíncrona pelo Worker do OrderService):
ORDER_ID="<orderId da resposta do POST>"
curl -s http://localhost:5001/orders/$ORDER_ID | jq '{status: .status}'Resposta esperada:
{"status": "Completed"}Use o header X-Simulate-Failure para injetar falhas em qualquer passo:
curl -s -X POST http://localhost:5001/orders \
-H "Content-Type: application/json" \
-H "X-Simulate-Failure: payment" \
-d '{"totalAmount": 50.00, "items": [{"productId": "PROD-001", "quantity": 1, "unitPrice": 50.00}]}' | jq .Cascata: PaymentProcessing → Failed (nenhuma compensação necessária — nada foi confirmado)
Após a saga terminar, GET /orders/{orderId} retorna "status": "Failed".
curl -s -X POST http://localhost:5001/orders \
-H "Content-Type: application/json" \
-H "X-Simulate-Failure: inventory" \
-d '{"totalAmount": 50.00, "items": [{"productId": "PROD-001", "quantity": 1, "unitPrice": 50.00}]}' | jq .Cascata: InventoryReserving → PaymentRefunding → Failed (estorno do pagamento)
Após a saga terminar, GET /orders/{orderId} retorna "status": "Failed".
curl -s -X POST http://localhost:5001/orders \
-H "Content-Type: application/json" \
-H "X-Simulate-Failure: shipping" \
-d '{"totalAmount": 50.00, "items": [{"productId": "PROD-001", "quantity": 1, "unitPrice": 50.00}]}' | jq .Cascata: ShippingCancelling → InventoryReleasing → PaymentRefunding → Failed (rollback completo)
Após a saga terminar, GET /orders/{orderId} retorna "status": "Failed".
bash scripts/happy-path-demo.shO script executa e valida automaticamente:
- Happy path completo (
Completedcom 4 transições) - Falha no pagamento (
Failedsem compensação) - Falha no inventário (
Failedcom estorno de pagamento) - Falha no shipping (
Failedcom cascata completa)
Saída de sucesso esperada:
✓ Cenário 1: Happy Path — OK
✓ Cenário 2: Falha no Pagamento — OK
✓ Cenário 3: Falha no Inventário — OK
✓ Cenário 4: Falha no Shipping — OK
4/4 cenários passaram.
# COM lock (padrão) — resultado correto: 2 Completed + 3 Failed
bash scripts/concurrent-saga-demo.sh
# SEM lock — demonstra overbooking (race condition TOCTOU)
bash scripts/concurrent-saga-demo.sh --no-lock
# Personalizar
bash scripts/concurrent-saga-demo.sh --pedidos 5 --estoque 2Nota sobre
--no-lock: requer reconfiguração do serviço comINVENTORY_LOCKING_ENABLED=falsenodocker-compose.ymle rebuild do container.
Com lock ativo (INVENTORY_LOCKING_ENABLED=true, padrão), o resultado esperado com estoque=2 e 5 pedidos:
Resultado: 2 Completed + 3 Failed
Estoque final: 0 (nenhum overbooking)
Sem lock, é possível observar mais de 2 sagas Completed com estoque insuficiente (overbooking).
Liste todas as mensagens nas Dead Letter Queues:
curl -s http://localhost:5002/dlq | jq .Resposta de exemplo:
[
{
"queueName": "payment-commands-dlq",
"messageId": "abc123",
"body": "{\"sagaId\":\"...\",\"commandType\":\"ProcessPayment\"}",
"approximateReceiveCount": "3",
"sentTimestamp": "1711900000000"
}
]Reenviar uma mensagem para a fila original:
curl -s -X POST http://localhost:5002/dlq/redrive \
-H "Content-Type: application/json" \
-d '{
"queueName": "payment-commands-dlq",
"receiptHandle": "<receiptHandle da mensagem acima>"
}' | jq .O projeto integra a stack LGTM (Grafana + Tempo + Loki) via um OTel Collector centralizado. Os 5 serviços .NET exportam traces e logs via OTLP gRPC para o Collector, que aplica tail sampling (descarta GET /health com status OK, mantém erros) e encaminha ao backend grafana/otel-lgtm (all-in-one).
Acesse http://localhost:3000 após docker compose up -d. Use Explore → Tempo para buscar traces por service.name ou saga.id, e Explore → Loki com {service_name="<serviço>"} para logs. Os datasources têm link bidirecional: um TraceID no log navega direto ao trace, e um span no Tempo mostra os logs correlacionados.
Os logs do ILogger<T> são exportados via OTLP com correlação automática de TraceId/SpanId (método AddSagaLogging() em Shared/Extensions/ServiceCollectionExtensions.cs). Cada serviço tem OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4317 e OTEL_SERVICE_NAME em kebab-case configurados no docker-compose.yml. Sem o endpoint definido, o console exporter é usado como fallback.
O projeto inclui uma suíte de 8 testes de integração end-to-end que sobe o ambiente completo via Docker Compose e valida todos os cenários principais.
Além dos pré-requisitos gerais, é necessário o SDK .NET 10:
dotnet --version # deve retornar 10.xdotnet test tests/IntegrationTests/O runner cuida de tudo automaticamente:
- Builda as imagens Docker (usa cache quando possível)
- Sobe LocalStack, PostgreSQL e os 5 serviços
- Aguarda as filas SQS serem criadas e os health checks passarem
- Executa os 8 testes em sequência
- Derruba e limpa o ambiente (
down -v)
Tempo esperado na primeira execução (build das imagens): ~3–5 minutos.
Execuções subsequentes (imagens em cache): ~1–2 minutos.
| ID | Classe | Cenário |
|---|---|---|
| T1 | HappyPathTests |
Pedido válido — saga atinge Completed com todas as transições |
| T2 | CompensationTests |
Falha no pagamento — saga termina Failed sem compensação |
| T3 | CompensationTests |
Falha no inventário — Failed com PaymentRefunding |
| T4 | CompensationTests |
Falha no shipping — Failed com cascata completa |
| T5a | IdempotencyTests |
Dois pedidos simultâneos — ambos completam sem corrupção de estado |
| T5b | IdempotencyTests |
Pedido falho não corrompe pedido bem-sucedido concorrente |
| T6 | ConcurrencyTests |
5 pedidos simultâneos com estoque=2 e lock pessimista — exatamente 2 completam |
| T7 | ConcurrencyTests |
Comportamento sem lock (documentacional — sempre passa) |
Aprovado IntegrationTests.Tests.CompensationTests.PaymentFailure_SagaFails_NoCompensation
Aprovado IntegrationTests.Tests.CompensationTests.ShippingFailure_SagaFails_InventoryAndPaymentCompensated
Aprovado IntegrationTests.Tests.CompensationTests.InventoryFailure_SagaFails_PaymentRefunded
Aprovado IntegrationTests.Tests.ConcurrencyTests.WithoutLock_DocumentsBehavior_AlwaysPasses
Aprovado IntegrationTests.Tests.ConcurrencyTests.WithPessimisticLock_ExactlyTwoComplete_NoOverbooking
Aprovado IntegrationTests.Tests.HappyPathTests.PostOrder_ValidProduct_SagaCompletes
Aprovado IntegrationTests.Tests.IdempotencyTests.TwoConcurrentOrders_BothComplete_NoStateCorruption
Aprovado IntegrationTests.Tests.IdempotencyTests.FailingOrderDoesNotCorruptConcurrentSuccessfulOrder
Total de testes: 8 | Aprovados: 8
Os testes usam um compose override (tests/IntegrationTests/docker-compose.test.yml) que:
- Fixa as portas 5001–5005 para os serviços
- Força
INVENTORY_LOCKING_MODE=pessimistic(necessário para T6) - Adiciona
restart: unless-stoppedpara tolerar o race entre o startup dos serviços e a criação assíncrona das filas SQS peloinit-sqs.sh
saga-orchestration-dotnet-sqs/
│
├── src/ # Código-fonte dos serviços .NET
│ ├── OrderService/ # API HTTP — recebe pedidos e inicia sagas
│ ├── SagaOrchestrator/ # Orquestrador — maquina de estados + DLQ endpoints
│ ├── PaymentService/ # Worker SQS — processa/compensa pagamentos
│ ├── InventoryService/ # Worker SQS — reserva/libera estoque (SELECT FOR UPDATE)
│ ├── ShippingService/ # Worker SQS — agenda/cancela entregas
│ └── Shared/ # Contracts, SQS helpers, Idempotency, OpenTelemetry
│
├── docs/ # Documentação didática (8 artigos)
│
├── scripts/ # Scripts bash para demos
│ ├── lib/common.sh # Funções compartilhadas (check_health, poll_saga, etc.)
│ ├── happy-path-demo.sh # 4 cenários sequenciais com verificação automática
│ └── concurrent-saga-demo.sh # Demo de concorrência com/sem lock
│
├── tests/
│ └── IntegrationTests/ # Suíte de 8 testes E2E (xUnit + Docker Compose)
│ ├── Tests/ # HappyPath, Compensation, Idempotency, Concurrency
│ ├── Infrastructure/ # DockerComposeFixture, SagaClient, InventoryClient
│ └── docker-compose.test.yml # Override de portas e locking mode para testes
│
├── infra/ # Configuração de infraestrutura local
│ ├── localstack/ # Init script de criação das filas SQS
│ ├── postgres/ # Init SQL com schemas do PostgreSQL
│ ├── otel/ # Configuração do OTel Collector
│ │ ├── otelcol.yaml # Receivers, processors, exporters e pipelines
│ │ └── processors/sampling/ # Políticas de tail sampling (drop health checks, keep errors)
│ └── grafana/ # Grafana provisioning automático
│ ├── provisioning/
│ │ ├── datasources/ # Datasources Tempo e Loki (com link bidirecional)
│ │ └── dashboards/ # Provider de dashboards
│ └── dashboards/ # Dashboard "Saga Orchestration — Overview" (JSON)
│
└── docker-compose.yml # Orquestração completa do ambiente local
| Serviço | Porta | Health Check |
|---|---|---|
| LocalStack (SQS) | 4566 | curl http://localhost:4566/_localstack/health |
| PostgreSQL | 5432 | — (acesso interno) |
| Grafana (LGTM) | 3000 | curl http://localhost:3000/api/health |
| OTel Collector (gRPC) | 4317 | — (ingress OTLP) |
| OTel Collector (HTTP) | 4318 | — (ingress OTLP) |
| OrderService | 5001 | curl http://localhost:5001/health |
| SagaOrchestrator | 5002 | curl http://localhost:5002/health |
| PaymentService | 5003 | curl http://localhost:5003/health |
| InventoryService | 5004 | curl http://localhost:5004/health |
| ShippingService | 5005 | curl http://localhost:5005/health |
Endpoints adicionais do InventoryService:
GET http://localhost:5004/inventory/stock/{productId} # Consultar estoque
POST http://localhost:5004/inventory/reset # Resetar estoque para demosOito artigos em docs/ que aprofundam cada padrão implementado:
| Documento | O que cobre |
|---|---|
| 01-fundamentos-sagas.md | Saga vs 2PC, orquestrada vs coreografada, justificativa da escolha |
| 02-maquina-de-estados.md | Diagrama completo de estados, transições forward e de compensação |
| 03-padroes-compensacao.md | Cascata reversa, CompensationDataJson, implementação do rollback |
| 04-idempotencia-retry.md | IdempotencyStore com Npgsql, chaves por saga, visibility timeout |
| 05-sqs-dlq-visibility.md | Topologia de filas, RedrivePolicy, endpoints GET/POST /dlq |
| 06-opentelemetry-traces.md | W3C TraceContext sobre SQS, SagaActivitySource, exporters OTLP, stack LGTM |
| 07-concorrencia-sagas.md | Race conditions, pessimistic vs optimistic locking, SELECT FOR UPDATE |
| 08-guia-pratico.md | Passo a passo completo de todos os cenários, troubleshooting |