A reserva de viagem
Você monta uma viagem em três reservas: voo, hotel e carro. Cada uma é confirmada na hora, em empresas diferentes — não existe um 'botão único' que reserve tudo ou nada. Se a locadora não tem carro na data, você não deixa o resto pendurado: liga para o hotel e cancela, depois cancela o voo (pagando eventual taxa). Nada disso 'volta no tempo' — o hotel chegou a ter sua reserva por alguns minutos — mas, no fim, o mundo fica como se a viagem nunca tivesse sido marcada. É exatamente o que uma Saga faz: confirma passo a passo e, na falha, desfaz por compensação, na ordem inversa.
O problema
Uma operação de negócio em microsserviços atravessa vários bancos: reservar estoque, cobrar pagamento, agendar entrega. Cada serviço garante ACID só dentro de casa — não existe commit/rollback que abranja o conjunto.
O two-phase commit (2PC) promete atomicidade distribuída, mas trava recursos enquanto todos votam, cria um coordenador que vira gargalo e não sobrevive a fluxos longos ou a participantes externos (a operadora de cartão não entra na sua transação).
A raiz do problema é o teorema de que consistência forte distribuída custa disponibilidade e latência: para o 2PC funcionar, cada participante segura locks até o coordenador decidir — em fluxos de segundos ou minutos, isso significa linhas de banco travadas, throughput despencando e um ponto único cuja queda deixa todos em estado 'preparado' sem saber o desfecho. A Saga aceita o trade-off oposto: libera cada recurso imediatamente (commit local) e paga o preço em isolamento.
Esse preço tem nome: estados intermediários visíveis. Entre o passo 1 e a compensação, outro cliente pode ver o estoque reservado de um pedido que vai falhar. A Saga entrega atomicidade SEMÂNTICA (no fim, tudo aconteceu ou nada 'vale') e consistência eventual, mas não entrega o I de ACID — quem precisa esconder intermediários usa contramedidas como reservas explícitas, status 'pendente' e bloqueio semântico no modelo de negócio.
A escolha entre orquestração e coreografia define quem carrega essa complexidade. Orquestração: um componente central conhece a receita, comanda cada serviço e decide compensar — o fluxo é explícito, testável e observável, mas o orquestrador concentra acoplamento e precisa ser resiliente (persistir o estado da saga para retomar após crash). Coreografia: cada serviço reage a eventos e publica os seus ('PagamentoFalhou' faz o Estoque se auto-liberar) — não há ponto central, mas a receita fica espalhada, e responder 'em que passo esse pedido está?' exige rastreamento distribuído. Regra prática: poucos passos e times autônomos favorecem coreografia; fluxos longos com decisões condicionais favorecem orquestração.
- Pedido
- passo 1 okReservar estoque
- passo 2 falhaCobrar pagamento
- desfaz na ordem inversaCompensar: liberar estoque
- estado final consistenteSaga abortada
Navegue pelas peças
Camada 1/4Orquestrador / Coreografia
use ← →Camada 1
Orquestrador / Coreografia
quem conhece a receita e decide compensar
Na orquestração, um componente central persiste o estado da saga, envia comandos e reage a respostas — a receita mora em um lugar só. Na coreografia, a receita se distribui: cada serviço assina eventos e sabe apenas seu próximo passo. Em ambos, alguém precisa decidir quando o fluxo avança e quando reverte.
No código
await executarSaga([reservarEstoque, cobrarPagamento, agendarEntrega]);Se violar: sem coordenação definida, cada falha vira decisão ad-hoc: passos ficam órfãos, meio-executados, e ninguém sabe se o pedido terminou ou travou.
Estrutura
Código
interface Passo {
executar(): Promise<void>;
compensar(): Promise<void>;
}
// Orquestrador simples: executa em ordem e compensa ao falhar
async function executarSaga(passos: Passo[]): Promise<void> {
const feitos: Passo[] = [];
try {
for (const passo of passos) {
await passo.executar();
feitos.push(passo);
}
} catch (erro) {
// compensa na ordem inversa dos passos concluídos
for (const passo of feitos.reverse()) {
await passo.compensar();
}
throw erro;
}
}
const reservarEstoque: Passo = {
executar: async () => { /* reserva */ },
compensar: async () => { /* libera reserva */ },
};from dataclasses import dataclass
from typing import Callable, Awaitable
import asyncio
@dataclass
class Passo:
executar: Callable[[], Awaitable[None]]
compensar: Callable[[], Awaitable[None]]
async def executar_saga(passos: list[Passo]) -> None:
feitos: list[Passo] = []
try:
for passo in passos:
await passo.executar()
feitos.append(passo)
except Exception:
# compensa na ordem inversa
for passo in reversed(feitos):
await passo.compensar()
raise
reservar_estoque = Passo(
executar=lambda: asyncio.sleep(0), # reserva
compensar=lambda: asyncio.sleep(0), # libera reserva
)Casos de uso
Checkout de e-commerce distribuído
Cenário
No fechamento do pedido, quatro serviços entram em cena: Pedido, Estoque, Pagamento e Entrega — cada um com seu banco, e o Pagamento ainda depende da operadora de cartão.
Como o padrão entra
Uma saga orquestrada sequencia reservar estoque → cobrar → agendar entrega. Cartão recusado? O orquestrador dispara 'liberar reserva' e marca o pedido como falho — o cliente vê um erro limpo, não um pedido zumbi com estoque preso.
Trade-off: Entre a reserva e a cobrança, o item aparece indisponível para outros clientes mesmo que a compra vá falhar — janela de isolamento fraco que times de estoque precisam aceitar e monitorar.
Abertura de conta bancária
Cenário
Onboarding digital: verificar KYC no serviço de compliance, criar a conta no core bancário, emitir o cartão em um parceiro externo. O KYC pode levar minutos e o parceiro tem instabilidade conhecida.
Como o padrão entra
Saga de longa duração: cada etapa commita localmente e o estado persiste ('KYC aprovado, conta criada, cartão pendente'). Se a emissão do cartão falha em definitivo, compensa-se encerrando a conta recém-criada e notificando o cliente — sem contas fantasma no core.
Trade-off: Compensar 'conta criada' não é apagar: regulação exige trilha de auditoria, então a compensação é um encerramento formal com registro — mais caro de projetar que um simples delete.
Booking de viagem
Cenário
Uma plataforma vende o pacote voo + hotel + carro consumindo três fornecedores externos, cada um com API, prazos de cancelamento e políticas próprias.
Como o padrão entra
A saga reserva em sequência e guarda, por passo, o identificador de cancelamento do fornecedor. Sem carro disponível, cancela hotel e voo pelos códigos guardados — o cliente recebe 'pacote indisponível' em vez de um voo solto que ele não pediu.
Trade-off: Compensação com custo real: fornecedores cobram taxa de cancelamento ou têm janelas de gratuidade — a ordem dos passos vira decisão financeira (reserve por último o mais caro de desfazer).
A compensação que não é idempotente
Compila, roda, passa no code review. E quebra depois.
O caminho feliz foi pensado com cuidado. A compensação foi escrita depois, às pressas, e assume que roda exatamente uma vez — que é justamente o que um sistema distribuído não garante.
async function compensarReserva(pedidoId: string) {
const reserva = await repo.buscar(pedidoId);
// Devolve ao estoque. Sem verificar se ja devolveu.
await estoque.incrementar(reserva.sku, reserva.quantidade);
}
async function compensarCobranca(pedidoId: string) {
const cobranca = await repo.buscarCobranca(pedidoId);
// Estorna. Sem chave de idempotencia.
await gateway.estornar(cobranca.transacaoId, cobranca.valor);
}
// A saga executa a compensacao, o ACK se perde na rede,
// o orquestrador reexecuta por seguranca.
// Resultado: 2 unidades voltaram ao estoque onde so 1 saiu,
// e o cliente foi estornado duas vezes.O que quebra
- Com ACK perdidoO orquestrador reexecuta a compensação e o efeito é aplicado duas vezes — estoque inflado, estorno duplicado.
- Na conciliaçãoO saldo diverge de um jeito que não bate com nenhum evento registrado, e ninguém consegue explicar de onde veio.
- No incidenteReprocessar a fila de compensações para 'garantir' amplifica o dano em vez de corrigi-lo.
A saída: Compensação é o passo que mais precisa ser idempotente, porque ela roda justamente quando o sistema já está instável. Toda compensação carrega chave de idempotência e verifica o estado antes de agir — 'já compensei este pedido?' vem antes de 'compensar'.
Armadilhas
Compensação não-idempotente
Mensageria com entrega at-least-once VAI reentregar comandos de compensação — e um estorno executado duas vezes devolve o dinheiro em dobro. Toda compensação precisa de chave de idempotência (estornar POR pedido, não 'estornar X reais') e de checagem de estado antes de agir: liberar uma reserva já liberada deve ser um no-op silencioso.
Passo não-compensável no meio do fluxo
Enviar o e-mail 'compra confirmada!' como passo 2 de 4 é irreversível: se o passo 3 falhar, não existe des-enviar. Ações sem compensação (e-mails, webhooks para terceiros, transferências liquidadas) devem ir para o fim da saga, depois do último ponto de falha possível — ou serem redesenhadas ('processando compra' antes, confirmação só no sucesso).
Saga sem timeout
Se o serviço de pagamento simplesmente não responde, a saga sem timeout fica 'executando' para sempre — estoque preso, pedido pendurado, cliente no limbo. Cada passo precisa de prazo máximo e de um desfecho padrão ao estourá-lo (normalmente compensar tudo). Cuidado com o clássico: o timeout dispara a compensação e a resposta atrasada chega depois — o desenho precisa decidir quem vence.
Quando usar × quando evitar
Use quando
4- Uma operação de negócio precisa de atomicidade entre vários serviços com bancos separados.
- O fluxo é longo ou depende de sistemas externos, tornando 2PC inviável.
- Você aceita consistência eventual e consegue definir compensações claras por passo.
- Já usa arquitetura orientada a eventos ou CQRS e quer coordenar processos de negócio.
Evite quando
3- Tudo cabe em uma única transação ACID local — não há por que distribuir.
- O domínio exige isolamento forte e não tolera estados intermediários visíveis.
- Não é possível compensar passos de forma segura (efeitos irreversíveis sem contrapartida).
Testar o que ficou
3 armadilhas de Saga viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Step Functions / TemporalOrquestradores de fluxo de trabalho encadeiam passos entre serviços e disparam compensações quando um deles falha no meio.checkout de e-commerceReservar estoque, cobrar e despachar são passos em serviços distintos; se a cobrança falha, a reserva é desfeita por uma ação compensatória.reserva de viagem (voo + hotel)Sem transação global entre companhias, cada reserva confirmada é cancelada por compensação quando a etapa seguinte não fecha.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Passos + compensações; sem 2PC entre serviços.
await reserva; try { await cobra; } catch { await libera; }O que ele cobra
2 saltos a mais entre a chamada e o efeito.
- Cada passo precisa de uma ação de compensação que desfaça o que ele fez
- Existe uma janela em que o sistema está parcialmente aplicado, visível a quem observa
Não vale se: os passos cabem numa transação local do mesmo banco — aí uma transação ACID resolve sem inventar compensações.