Sagas vs 2PC: transações distribuídas sem perder o sono
- ⬜🔁 Idempotência e Retries: o antídoto pra rede que quebra(Sistemas Distribuídos)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Você tem um microserviço de pedidos, um de pagamento, um de estoque. Um pedido precisa: reservar estoque, cobrar o cartão, agendar entrega. Se qualquer um falha, você precisa desfazer o que foi feito. Mas cada serviço tem seu próprio banco — não existe BEGIN TRANSACTION global. O que fazer?
Duas respostas sérias: Two-Phase Commit (2PC), a solução clássica (e bloqueante), e Sagas, a solução moderna baseada em compensações. Este módulo explica as duas, quando cada uma faz sentido, e como implementar Sagas corretamente com orchestration, choreography e o outbox pattern.
Por que transações distribuídas são difíceis
Numa tabela única, o banco te dá ACID de graça. Atomicidade, consistência, isolamento, durabilidade — tudo garantido. Em sistemas distribuídos com vários serviços independentes, você perde:
| Propriedade | Monolito/Single DB | Microserviços/Multi DB |
|---|---|---|
| Atomicidade | BEGIN/COMMIT do banco | Precisa coordenar múltiplos commits |
| Isolamento | Locks locais + MVCC | Não existe — cada banco só vê o próprio |
| Consistência global | Constraints + FKs | Impossível; precisa de eventual consistency |
| Durabilidade | WAL + fsync | Por banco; o problema é orquestração |
O desafio real não é só técnico — é de semântica de negócio. Se "cobrar cartão" funciona mas "agendar entrega" falha, você precisa reembolsar. Isso não é infraestrutura, é fluxo de negócio que precisa ser codificado.
Two-Phase Commit (2PC)
2PC é o protocolo clássico, usado por XA (X/Open), bancos Oracle/SQL Server com DTC, JTA em Java. Dois papéis: coordinator e participants.
┌─────────────┐
│ COORDINATOR │
└─────────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Payment DB│ │ Order DB │ │ Stock DB │
│(participant)│ │(participant)│ │(participant)│
└───────────┘ └───────────┘ └───────────┘
FASE 1: PREPARE
coordinator ──► participants: "podem commitar?"
participants ──► executam local, seguram locks, fsync WAL, respondem "YES" ou "NO"
FASE 2: COMMIT (se todos disseram YES) ou ABORT (se algum disse NO)
coordinator ──► participants: "commit!" (ou "abort!")
participants: aplicam ou descartam, soltam locksProblemas do 2PC em sistemas modernos:
| Problema | Impacto |
|---|---|
| Blocking protocol | Se coordinator cair entre prepare e commit, participants ficam com locks presos. Não podem decidir sozinhos. |
| Locks durante a janela | Em transações longas, locks mantidos bloqueiam outras transações — throughput morre. |
| Exige 2PC em TODOS participants | Kafka, MongoDB, Redis, REST APIs não suportam. 2PC só roda em bancos XA-compliant. |
| Performance em WAN | Duas rodadas de mensagens + fsync. Latência cresce com a pior latência do participant. |
| Complexidade operacional | Coordinator requer recovery log próprio, failover, monitoring. Grande fonte de dor. |
Saga Pattern: compensações > 2PC
Saga foi proposto em 1987 (Garcia-Molina & Salem) originalmente como otimização pra transações longas em banco único. Renasceu com microserviços como o padrão dominante. A ideia central:
Em vez de locks globais, você aceita que os dados temporariamente inconsistentes e desfaz com ações compensatórias. Não é rollback — é uma nova operação que anula a anterior.
PEDIDO FELIZ: 1. Reservar estoque ✓ 2. Cobrar cartão ✓ 3. Agendar entrega ✓ → ok, pedido confirmado PEDIDO QUE FALHA NO STEP 3: 1. Reservar estoque ✓ 2. Cobrar cartão ✓ 3. Agendar entrega ✗ → começa compensação COMPENSAÇÃO (ordem inversa): 3'. (nada a desfazer de agendar — nunca aconteceu) 2'. Reembolsar cartão ✓ 1'. Liberar estoque ✓ → pedido cancelado, sistema consistente
Orchestration: o coordinator explícito
Um serviço central (o orchestrator) conhece o workflow completo e comanda cada step explicitamente. É uma state machine.
┌──────────────────────────────┐ │ Order Saga Orchestrator │ │ (state machine + persist) │ └──────────────────────────────┘ │ ↑ │ ↑ │ ↑ │ ok │ │ ok │ │ ok │ ▼ │ ▼ │ ▼ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Stock │ │ Payment │ │Shipping │ │ Service │ │ Service │ │ Service │ └─────────┘ └─────────┘ └─────────┘ Fluxo: - Orchestrator chama Stock.reserve → ok - chama Payment.charge → ok - chama Shipping.schedule → FALHA - chama Payment.refund (compensação) - chama Stock.release (compensação) - marca saga FAILED, notifica cliente
Implementação com Temporal (workflow engine):
# order_saga_workflow.py — orchestration com Temporal (temporal.io)
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy
@workflow.defn
class OrderSagaWorkflow:
@workflow.run
async def run(self, order: dict) -> dict:
# Passo 1: reservar estoque
reservation_id = await workflow.execute_activity(
stock_reserve, order, start_to_close_timeout=timedelta(seconds=30),
retry_policy=RetryPolicy(maximum_attempts=5),
)
try:
# Passo 2: cobrar cartão
charge_id = await workflow.execute_activity(
payment_charge, order, start_to_close_timeout=timedelta(seconds=30),
)
try:
# Passo 3: agendar entrega
shipment_id = await workflow.execute_activity(
shipping_schedule, order, start_to_close_timeout=timedelta(seconds=30),
)
return {"status": "ok", "order_id": order["id"]}
except Exception as e:
# compensa: reembolsar cartão
await workflow.execute_activity(
payment_refund, charge_id,
start_to_close_timeout=timedelta(seconds=60),
retry_policy=RetryPolicy(maximum_attempts=10), # crítico, retria muito
)
raise
except Exception as e:
# compensa: liberar estoque
await workflow.execute_activity(
stock_release, reservation_id,
start_to_close_timeout=timedelta(seconds=60),
)
raiseChoreography: eventos puros, sem orquestrador
Cada serviço publica um evento após concluir sua parte. Outros serviços escutam e reagem. Não há um coordinator central — a "lógica do workflow" está distribuída.
┌─────────────┐ OrderCreated ┌───────────┐
│ Order API │──────────────────────►│ Kafka │
└─────────────┘ │ (events) │
└───────────┘
│ │ │
┌──────────────────────────────┘ │ └─────────────────────────┐
▼ ▼ ▼
┌─────────────────────┐ ┌────────────────────────┐ ┌───────────────────┐
│ Stock Service │ │ Payment Service │ │ Shipping Service │
│ on(OrderCreated): │ │ on(StockReserved): │ │ on(PaymentOK): │
│ reserve + publish │ │ charge + publish │ │ schedule+publish │
│ StockReserved │ │ PaymentSucceeded │ │ ShipmentScheduled│
└─────────────────────┘ └────────────────────────┘ └───────────────────┘
Se Payment falhar:
publica PaymentFailed
├─► Stock Service reage: on(PaymentFailed) → release reservation
└─► Order Service reage: on(PaymentFailed) → mark order FAILED| Aspecto | Orchestration | Choreography |
|---|---|---|
| Acoplamento | Orchestrator conhece todos | Serviços desacoplados via eventos |
| Visibilidade do workflow | Alta (state machine explícita) | Baixa — lógica espalhada em N listeners |
| Adicionar novo step | Altera o orchestrator | Adiciona novo listener, talvez publica evento novo |
| Debug | Trace do orchestrator | Pesadelo — precisa de distributed tracing forte |
| Performance | Latência = soma dos passos | Geralmente paralelo, pode ser mais rápido |
| Risco | SPOF no orchestrator (mitigado com HA) | "Spaghetti de eventos" conforme cresce |
| Quando usa | Workflows complexos, multi-step | Fluxos simples, baixo acoplamento |
Compensating actions: o que é e o que não é
Compensação ≠ rollback. É uma nova operação que anula o efeito visível da anterior, mas pode deixar rastros auditáveis.
| Ação original | Compensação adequada | Nota |
|---|---|---|
| Reservar estoque (INSERT) | Liberar reserva (DELETE ou UPDATE status=released) | Simples |
| Cobrar cartão (charge) | Reembolsar (refund) | Cria TX nova no gateway — auditável |
| Agendar entrega | Cancelar entrega + talvez notificar motorista | Se já despachou, compensação vira "recall" |
| Enviar email ao cliente | (não-compensável: enviar email corrigido) | Texto: "desconsidere a mensagem anterior" |
| Gerar nota fiscal | Emitir nota de cancelamento | Fiscalmente relevante — não apague o original |
- Idempotente: o orchestrator pode retriar a compensação se cair no meio. Chamar 5 vezes → mesmo efeito final.
- Commutativa no worst case: idealmente, ordem de compensações não importa (nem sempre possível).
- Sempre publicada: logs + eventos de compensação pra auditoria. Nunca "apaga" silenciosamente.
Tratando o worst case: semantic lock + pivot transaction
Alguns sagas têm um ponto de não-retorno: operações que se foram, foram. Ex: "despachar container pro navio". Padrões:
| Padrão | Uso |
|---|---|
| Semantic lock | Marca o registro como "pendente" (não-cancelável). Outras operações veem o lock e respeitam. Saga libera no final. |
| Pivot transaction | A última transação "cara" da saga. A partir dela, sem retorno. Antes dela, cancela fácil. |
| Compensable transactions | Steps que podem ser compensados sem perda (reserva, hold). |
| Retriable transactions | Steps após o pivot que precisam eventualmente acontecer (retry com backoff até sucesso). |
[C] Reservar estoque ← compensable (pode liberar) [C] Cobrar cartão ← compensable (pode reembolsar) ──── PIVOT ──────────────── [P] Despachar pro carrier ← pivot: depois daqui, sem retorno [R] Gerar nota fiscal ← retriable (precisa acontecer sempre) [R] Notificar cliente ← retriable Se step [P] falhar: compensa [C]'s. Se [R] falhar: retria infinitamente (pedido já foi).
Outbox pattern: garantindo que eventos saem
Num saga de choreography (ou orchestration com eventos), o outbox patterngarante que a operação local e o evento são atômicos:
# order_service.py — outbox garantindo atomicidade
async def create_order(data: dict) -> dict:
async with db.begin() as tx:
# 1. Inserir pedido na tabela de domínio
order = await tx.execute(
text("INSERT INTO orders (...) VALUES (...) RETURNING *"),
data,
)
order_row = order.one()
# 2. Inserir evento na outbox — MESMA transação
await tx.execute(
text("""
INSERT INTO outbox (aggregate_id, event_type, payload, created_at)
VALUES (:id, 'OrderCreated', :payload, NOW())
"""),
{"id": order_row.id, "payload": json.dumps(data)},
)
# commit = ambos salvos ou nenhum
return {"id": order_row.id}
# relay.py — worker separado publicando
async def relay_loop():
while True:
async with db.begin() as tx:
rows = await tx.execute(text("""
SELECT id, event_type, payload FROM outbox
WHERE sent_at IS NULL
ORDER BY id
LIMIT 100
FOR UPDATE SKIP LOCKED
"""))
events = rows.fetchall()
for e in events:
await kafka.publish(topic="orders", key=str(e.id), value=e.payload)
await tx.execute(
text("UPDATE outbox SET sent_at = NOW() WHERE id = :id"),
{"id": e.id},
)
await asyncio.sleep(0.1)Alternativa mais escalável: CDC (Change Data Capture) com Debezium lendo o WAL do Postgres. Zero polling, latência baixa. Mas acopla pipeline à schema da tabela.
Decisões reais
📋 Workflow de checkout e-commerce com 4-5 serviços, sub-segundo
Você precisa de visibilidade (qual step falhou?), retries configuráveis por step, e evolução de workflow (adicionar passo = editar state machine). Temporal ou Step Functions te dão isso out-of-the-box. 2PC não serve porque seus serviços não são XA-compliant, e escalar travas globais mataria o throughput.
Alt: Saga Choreography — Se o fluxo é simples e desacoplado. Vira complexo rápido.
Alt: 2PC com XA — Só se você já tem Oracle/SQL Server + bancos XA — hoje é raro.
📋 Fluxo longo: onboarding de cliente B2B (dias, 10+ steps, aprovações humanas)
Workflows longos com espera por humanos, timeouts de dias, retries caso backoffice demore — Temporal foi literalmente feito pra isso. Camunda BPM é melhor se você quer visualização BPMN pra stakeholders não-técnicos.
Alt: Cron jobs + status polling — Funciona em fluxos triviais; vira inferno com 10 steps.
Alt: AWS Step Functions — Boa opção se já usa AWS e fluxo é visual.
📋 Microserviços pequenos com eventos simples, sem dependências complexas
Se seu workflow é <5 steps e você não precisa de visibilidade centralizada, choreography é mais barato e desacoplado. Cada serviço reage a eventos via consumer group. Cuidado pra não virar spaghetti conforme cresce — considere Orchestration antes do segundo cruzamento de eventos.
Alt: Orchestration — Quando o workflow passa de 5 steps ou você perde visibilidade.
Perguntas típicas (Q&A)
Sagas substituem ACID?
Não. Sagas dão semantic consistency (eventual), não ACID strict. Durante a saga, outras queries podem ver estado intermediário inconsistente. Se a sua lógica de negócio exige isolamento estrito, Saga não é a resposta — repense o design (talvez um serviço só).
Como testar Sagas?
Testes determinísticos: Temporal tem replay de workflow. Testes de caos: force cada step a falhar em todos os pontos possíveis, verifique que compensações rodam. Falha mid-compensation também — seu orchestrator deve retomar após crash.
O que é "Saga inválida"?
Quando uma compensação falha e não pode ser resolvida (ex: cliente deletou conta antes do reembolso processar). Nesses casos, você entra em modo manual: alerta pra operador, deadletter queue, compensação humana. Design pra isso acontecer.
Múltiplas sagas ao mesmo tempo no mesmo registro — race condition?
Possível. Solução: optimistic locking (version column + CAS) no agregado. Se duas sagas tentam modificar a mesma reserva, a segunda falha e retria (ou compensa).
Kafka Transactions são 2PC disfarçado?
Kinda. Kafka EOS usa transactional producer com 2PC entre produtor e broker. É 2PC dentro do Kafka, mas não te ajuda em sagas que atravessam DB + Kafka — pra isso precisa de outbox.
- 2PC é ACID distribuído, mas é blocking e exige participants XA-compliant. Raro em microserviços modernos.
- Saga = sequência de transações locais + compensações. Sem locks globais, mas só semantic consistency.
- Orchestration: central, explícito, fácil de debugar. Use Temporal/Step Functions/Camunda.
- Choreography: eventos, desacoplado, fácil de virar spaghetti. Comece com Orchestration por default.
- Compensações são novas operações, não rollback. Sempre idempotentes, auditáveis.
- Identifique o pivot transaction: depois dela, sem retorno — steps posteriores são retriable infinitamente.
- Outbox pattern é a cola: garante que domain write + evento sejam atômicos.
Próximo módulo: e se os eventos forem a fonte da verdade? Event Sourcing e CQRS.
Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito