🧠FFVAcademy
🪢

Sagas vs 2PC: transações distribuídas sem perder o sono

17 min de leitura·+85 XP
Pré-requisitos (0/1)0%

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:

PropriedadeMonolito/Single DBMicroserviços/Multi DB
AtomicidadeBEGIN/COMMIT do bancoPrecisa coordenar múltiplos commits
IsolamentoLocks locais + MVCCNão existe — cada banco só vê o próprio
Consistência globalConstraints + FKsImpossível; precisa de eventual consistency
DurabilidadeWAL + fsyncPor 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 locks

Problemas do 2PC em sistemas modernos:

ProblemaImpacto
Blocking protocolSe coordinator cair entre prepare e commit, participants ficam com locks presos. Não podem decidir sozinhos.
Locks durante a janelaEm transações longas, locks mantidos bloqueiam outras transações — throughput morre.
Exige 2PC em TODOS participantsKafka, MongoDB, Redis, REST APIs não suportam. 2PC só roda em bancos XA-compliant.
Performance em WANDuas rodadas de mensagens + fsync. Latência cresce com a pior latência do participant.
Complexidade operacionalCoordinator requer recovery log próprio, failover, monitoring. Grande fonte de dor.
⚠️
3PC (Three-Phase Commit) adiciona uma fase "pre-commit" pra mitigar o blocking. Ainda é vulnerável a partições de rede, quase ninguém implementa em produção. Na prática, sistemas que precisam de 2PC em 2026 são minoria absoluta (bancos legacy, mainframe). Todo o mundo greenfield foi pra Sagas.

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:

💡
Saga: uma transação distribuída é uma sequência de transações locais (uma por serviço). Se alguma falha, o sistema executa compensaçõesem ordem inversa pra desfazer efeitos já aplicados.

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
⚠️
Cuidado: a Saga não é ACID. Durante a saga, outras operações podem ver estados intermediários (ex: dinheiro já debitado mas pedido cancelado 100ms depois). O termo é semantic consistency — você garante consistência eventual com regras de negócio, não isolamento estrito.

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):

python
# 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),
            )
            raise
💡
Por que Temporal/Cadence e não orchestrator próprio? Workflow engines persistem o estado após cada step automaticamente. Se o processo crashar, na próxima boot o workflow retoma do último step confirmado. Implementar isso na mão é um projeto de 6 meses. Alternativas: AWS Step Functions, Camunda Zeebe, Netflix Conductor, Uber Cadence, dbos-inc.

Choreography: 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
AspectoOrchestrationChoreography
AcoplamentoOrchestrator conhece todosServiços desacoplados via eventos
Visibilidade do workflowAlta (state machine explícita)Baixa — lógica espalhada em N listeners
Adicionar novo stepAltera o orchestratorAdiciona novo listener, talvez publica evento novo
DebugTrace do orchestratorPesadelo — precisa de distributed tracing forte
PerformanceLatência = soma dos passosGeralmente paralelo, pode ser mais rápido
RiscoSPOF no orchestrator (mitigado com HA)"Spaghetti de eventos" conforme cresce
Quando usaWorkflows complexos, multi-stepFluxos simples, baixo acoplamento
⚠️
Regra prática: comece com Orchestration. É mais fácil de debugar e evoluir. Vá pra Choreography quando tem um fluxo claramente desacoplado e simples, ou quando o orchestrator virou gargalo. Muitos sistemas misturam os dois (orchestration no fluxo principal, choreography pra side effects como analytics/notificações).

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 originalCompensação adequadaNota
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 entregaCancelar entrega + talvez notificar motoristaSe já despachou, compensação vira "recall"
Enviar email ao cliente(não-compensável: enviar email corrigido)Texto: "desconsidere a mensagem anterior"
Gerar nota fiscalEmitir nota de cancelamentoFiscalmente relevante — não apague o original
🚨
3 regras de ouro pra compensação:
  • 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ãoUso
Semantic lockMarca o registro como "pendente" (não-cancelável). Outras operações veem o lock e respeitam. Saga libera no final.
Pivot transactionA última transação "cara" da saga. A partir dela, sem retorno. Antes dela, cancela fácil.
Compensable transactionsSteps que podem ser compensados sem perda (reserva, hold).
Retriable transactionsSteps 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:

python
# 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

Saga Orchestration com Temporal ou Step Functions

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 ChoreographySe o fluxo é simples e desacoplado. Vira complexo rápido.

Alt: 2PC com XASó se você já tem Oracle/SQL Server + bancos XA — hoje é raro.

📋 Fluxo longo: onboarding de cliente B2B (dias, 10+ steps, aprovações humanas)

Saga Orchestration com Temporal (ou Camunda pra BPM humano)

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 pollingFunciona em fluxos triviais; vira inferno com 10 steps.

Alt: AWS Step FunctionsBoa opção se já usa AWS e fluxo é visual.

📋 Microserviços pequenos com eventos simples, sem dependências complexas

Choreography com Kafka/EventBridge + outbox pattern

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: OrchestrationQuando 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.

Take-aways:
  • 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

Continue lendo