Capstone: saga distribuída ponta a ponta
- ⬜🚦 Rate Limiting Distribuído: token bucket, sliding window, Redis(Sistemas Distribuídos)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
O problema: transação através de serviços
Em monolito com DB único, transação é fácil: BEGIN; ...; COMMIT. Em microservices, cada serviço tem seu DB. Pedido (Order) + pagamento (Payment) + reserva de estoque (Inventory) + envio (Shipping) — 4 bancos diferentes, 4 times diferentes, 4 linguagens possivelmente.
Precisamos garantir que: ou todos os steps completam, ou nenhum efeito persiste. Os dois caminhos para isso: 2PC (Two-Phase Commit) e Saga.
Por que 2PC não serve em microservices
| Aspecto | 2PC | Saga |
|---|---|---|
| Modelo | ACID distribuído (all-or-nothing) | Eventual consistency com compensação |
| Acoplamento | Alto (XA drivers, coordinator) | Baixo (eventos/comandos) |
| Disponibilidade | Ruim (blocking se coordinator cai) | Alta (cada serviço autônomo) |
| Latência | p99 do mais lento dos N | Cada step independente |
| Cross-region | Inviável (RTT alto) | Natural |
| Debug | Fácil (transação atômica) | Difícil (estado intermediário visível) |
| Recomendação 2026 | Legacy RDBMS mesmo DC | Microservices, cross-region, cloud-native |
Blocking problem do 2PC: após prepare, cada participant trava seus recursos esperando commit do coordinator. Se coordinator morre, recursos ficam travados até admin intervir. Em um sistema com 100+ microservices, isso é outage diário.
Saga: definição e variantes
Saga = sequência de transações locais, cada uma commitada no seu serviço. Se algum step falha, rodamos compensações dos steps anteriores (ações semanticamente inversas).
Se Shipping falha (ex: endereço inválido), disparamos compensações no sentido inverso:
Choreography vs Orchestration: qual escolher?
📋 Microservices novos, time pequeno, 3-5 steps por saga
Baixo acoplamento, sem singleton crítico, cada serviço publica eventos e reage. Fácil adicionar novo consumer sem mudar ninguém.
Alt: Orchestration — mais overhead para equipe pequena. Vale a pena quando fluxo tem > 7 steps ou muitos conditionals
📋 10+ steps com lógica condicional, equipe grande, auditoria regulatória
Fluxo é visível em um único lugar (state machine), fácil auditar, debugar e modificar. Temporal dá durability out-of-box + retry + observability.
Alt: Choreography — vira difícil entender fluxo — precisa greppar eventos espalhados em 10 serviços
Implementação: orchestrator com Temporal
# Temporal workflow — state machine durável
from temporalio import workflow, activity
from datetime import timedelta
@activity.defn
async def create_order(input: OrderInput) -> str: ...
@activity.defn
async def cancel_order(order_id: str) -> None: ...
@activity.defn
async def charge_payment(order_id: str, amount: int, idempotency_key: str) -> str: ...
@activity.defn
async def refund_payment(charge_id: str, idempotency_key: str) -> None: ...
@activity.defn
async def reserve_inventory(order_id: str, items: list) -> str: ...
@activity.defn
async def release_inventory(reservation_id: str) -> None: ...
@workflow.defn
class CheckoutSaga:
@workflow.run
async def run(self, input: CheckoutInput) -> str:
compensations = []
try:
order_id = await workflow.execute_activity(
create_order, input,
start_to_close_timeout=timedelta(seconds=10),
retry_policy={"maximum_attempts": 3},
)
compensations.append(lambda: workflow.execute_activity(cancel_order, order_id))
charge_id = await workflow.execute_activity(
charge_payment, order_id, input.amount, input.idempotency_key,
start_to_close_timeout=timedelta(seconds=15),
)
compensations.append(lambda: workflow.execute_activity(
refund_payment, charge_id, input.idempotency_key
))
reservation_id = await workflow.execute_activity(
reserve_inventory, order_id, input.items,
start_to_close_timeout=timedelta(seconds=10),
)
compensations.append(lambda: workflow.execute_activity(release_inventory, reservation_id))
# ... mais steps
return order_id
except Exception as e:
# Compensação na ordem inversa
for compensate in reversed(compensations):
try:
await compensate()
except Exception as comp_error:
# Compensação falhou — escala pro humano
await workflow.execute_activity(
alert_ops, str(comp_error),
start_to_close_timeout=timedelta(seconds=5),
)
raiseTemporal resolve 3 problemas difíceis grátis: (1) durability — workflow sobrevive a crash de worker, continua do último step; (2) retry com backoff — configurável por activity; (3) timeouts + heartbeats — detecta activity travada.
Idempotência — não é opcional
Cada activity pode ser chamada 2+ vezes (timeout, retry, worker crash mid-operation). Sem idempotência, você cobra o cartão 3 vezes.
-- Tabela de idempotency por serviço
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
request_hash TEXT NOT NULL, -- SHA do payload
response JSONB NOT NULL,
status_code INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL -- TTL, ex: 24h
);
CREATE INDEX idx_idempotency_expires ON idempotency_keys (expires_at);
-- Lógica no handler:
-- 1. SELECT * FROM idempotency_keys WHERE key = $1
-- Se encontrou: retorna response salvo (comparar hash pra detectar payload diff)
-- Se não: processa + INSERT response + retornaasync def charge_payment_handler(request):
idempotency_key = request.headers.get("Idempotency-Key")
if not idempotency_key:
return Response(400, "Missing Idempotency-Key header")
# Lookup
existing = await db.fetchone(
"SELECT response, status_code, request_hash FROM idempotency_keys WHERE key = $1",
idempotency_key,
)
if existing:
# Se payload diferente com mesma key → erro (cliente bugado)
if existing["request_hash"] != hash_payload(request.body):
return Response(422, "Idempotency key reused with different payload")
return Response(existing["status_code"], existing["response"])
# Processa de verdade
result = await stripe.charge(request.body)
# Salva response
await db.execute(
"INSERT INTO idempotency_keys (key, request_hash, response, status_code, expires_at) "
"VALUES ($1, $2, $3, $4, $5)",
idempotency_key, hash_payload(request.body), result, 200, now() + timedelta(hours=24),
)
return Response(200, result)No capstone, o que precisa ser projetado com mais cuidado numa transação com compensação?
Observability e intervenção humana
- saga_id em todo log e trace — permite reconstruir jornada completa (Datadog APM, Grafana Tempo)
- State machine visível — dashboard mostra todas sagas ativas, stuck, compensating, failed
- DLQ pra compensação falha — separar compensações que não completaram pra alerta humano
- Interface admin — operador pode ver saga travada, executar compensação manualmente, marcar como resolvida
- Métricas RED por saga type: taxa de falha, latência end-to-end, taxa de compensação. Se compensação rate sobe, algo regrediu
- Runbook escrito: "se saga X falha em step Y, fazer Z" — operador não deveria pensar em incidente às 3am
Armadilhas fatais
Cinco erros que matam saga em produção. (1) Compensação não idempotente — o retry do estorno cobra o cliente duas vezes; toda compensação precisa de chave de idempotência, igual ao passo original. (2) Compensação que pode falhar sem plano B — se o estorno falha, você fica com inconsistência permanente e ninguém é avisado; compensação exige retry infinito, DLQ e alerta para humano. (3) Tratar compensar como rollback — e-mail enviado não volta, SMS não se desmanda; a semântica é compensar o efeito no mundo, não desfazer a transação. (4) Passo sem timeout — uma activity pendurada deixa a saga aberta para sempre, segurando recurso e mascarando o incidente. (5) Ignorar que saga não tem isolamento — outro processo lê o estado intermediário e toma decisão sobre dado que vai ser compensado (dirty read); precisa de contramedida explícita: semantic lock, update comutativo ou reread antes de confirmar.
Checklist: saga pronta pra produção
- Todas activities têm idempotency_key obrigatório
- Toda activity tem timeout + retry policy configurada
- Toda ação tem sua compensação semântica correspondente
- Compensação também é idempotente
- saga_id logado + traceado em cada activity
- DLQ configurada pra compensação falha + alerta
- Dashboard de sagas (ativas, stuck, failed)
- Interface admin pra intervenção manual
- Runbook escrito por tipo de falha
- Chaos test: kill worker durante saga, verificar resume correto
- Chaos test: força falha em step N, verificar compensação
- Budget de compensação por tipo (ex: refund > 5% do volume = alerta)
Take-aways
Saga não é "transação distribuída light" — é padrão fundamentalmente diferente baseado em eventual consistency + compensações semânticas. 2PC morreu em microservices. Saga bem feita (Temporal/Step Functions + idempotência + observability) é a forma correta de garantir consistência em sistemas distribuídos modernos.
Perguntas frequentes
❓ Por onde começar uma saga entre três serviços?
❓ Como testar uma saga?
❓ Como o usuário percebe uma saga que compensou?
Fixando
Como o capstone lida com o fato de que algumas ações não são compensáveis?
Qual é o requisito de observabilidade específico desse tipo de fluxo?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…