SAGA
O problema no contexto distribuído
Seção intitulada “O problema no contexto distribuído”Com database-per-service, uma operação como “criar pedido” pode envolver os serviços de Pedidos, Estoque, Pagamento e Entrega — cada um com seu próprio banco. Não existe um commit global que englobe os quatro. O 2PC (two-phase commit) até existe, mas mantém locks por toda a duração da transação distribuída, degradando disponibilidade e escala, e nem todo broker/serviço o suporta.
A saga troca atomicidade por consistência eventual: em vez de “tudo ou nada instantâneo”, temos “cada passo compromete localmente; se algo falhar depois, compensamos”. Durante a execução, o sistema fica temporariamente inconsistente (semi-committed) — o que exige atenção a isolamento.
Cada passo local respeita a fronteira transacional de um Agregado: o agregado é a maior unidade que ainda cabe em uma transação ACID única. A saga costura vários agregados de serviços diferentes.
A solução
Seção intitulada “A solução”- Modele a operação como N passos
Tᵢ, cada um uma transação local. - Para cada passo, defina (quando aplicável) uma compensação
Cᵢque desfaz semanticamenteTᵢ. - Execute
T₁ … Tₙem ordem. SeTₖfalhar, executeCₖ₋₁ … C₁em ordem inversa.
Compensações são semânticas, não rollback físico: você não “apaga” o pagamento, você estorna; não “desfaz” o e-mail enviado, você envia um cancelamento. Passos podem ser classificados como compensáveis, pivô (o ponto de não-retorno) e retriable (após o pivô, só avançam, com retry).
Orquestração vs coreografia
Seção intitulada “Orquestração vs coreografia”- Coreografia: não há coordenador central. Cada serviço reage a Domain Events e emite os seus. É Event-Driven Architecture pura — baixo acoplamento, mas o fluxo fica implícito e difícil de rastrear quando cresce.
- Orquestração: um orquestrador (uma máquina de estados) comanda explicitamente cada passo, dizendo a cada serviço o que fazer e reagindo às respostas. Fluxo centralizado e observável, ao custo de um componente a mais e risco de virar um “deus”.
Regra prática: poucos passos e times autônomos → coreografia; fluxos longos, com ramificações e necessidade de auditoria → orquestração.
Contraste com ACID
Seção intitulada “Contraste com ACID”| Aspecto | Transação ACID local | SAGA |
|---|---|---|
| Atomicidade | Garantida pelo banco | Emulada por compensação |
| Consistência | Imediata | Eventual |
| Isolamento | Serializável possível | Não há — precisa de contramedidas |
| Escopo | Um banco | Vários serviços |
A ausência de isolamento é o trade-off mais sutil: anomalias como dirty read (ler estado semi-committed) e lost update exigem contramedidas — flags de estado (ex.: pedido em PENDING), locks semânticos ou versionamento.
Trade-offs
Seção intitulada “Trade-offs”- Prós: sem locks distribuídos, alta disponibilidade, serviços desacoplados, escala.
- Contras: consistência eventual, complexidade de modelar compensações corretas, falta de isolamento, e a compensação em si pode falhar (precisa ser idempotente e ter retry infinito ou intervenção).
Quando NÃO usar
Seção intitulada “Quando NÃO usar”- Quando a operação cabe em um único serviço/agregado — use uma transação ACID local, muito mais simples.
- Quando o negócio não tolera janela de inconsistência (embora quase sempre tolere mais do que se imagina).
- Quando não há compensação semântica possível para um passo crítico e o passo não pode ser reordenado para depois do pivô.
Exemplo Java/Spring (orquestração)
Seção intitulada “Exemplo Java/Spring (orquestração)”Orquestrador como máquina de estados reagindo a comandos e replies. Cada passo publica via Message Outbox para garantir entrega confiável.
public enum SagaState { STARTED, INVENTORY_RESERVED, PAYMENT_DONE, COMPLETED, COMPENSATING_PAYMENT, COMPENSATING_INVENTORY, FAILED}
@Entitypublic class CreateOrderSaga { @Id private UUID id; @Enumerated(EnumType.STRING) private SagaState state; private UUID orderId; private BigDecimal amount;
protected CreateOrderSaga() {}
public CreateOrderSaga(UUID orderId, BigDecimal amount) { this.id = UUID.randomUUID(); this.orderId = orderId; this.amount = amount; this.state = SagaState.STARTED; }
public void advance(SagaState next) { this.state = next; } public SagaState state() { return state; } public UUID orderId() { return orderId; } public BigDecimal amount() { return amount; } public UUID id() { return id; }}
@Component@RequiredArgsConstructorpublic class CreateOrderOrchestrator {
private final SagaRepository sagas; private final CommandPublisher commands;
@Transactional public void start(UUID orderId, BigDecimal amount) { var saga = new CreateOrderSaga(orderId, amount); sagas.save(saga); commands.send(new ReserveInventory(saga.id(), orderId)); }
@Transactional public void on(InventoryReserved reply) { var saga = sagas.getById(reply.sagaId()); saga.advance(SagaState.INVENTORY_RESERVED); commands.send(new ProcessPayment(saga.id(), saga.orderId(), saga.amount())); }
@Transactional public void on(PaymentDone reply) { var saga = sagas.getById(reply.sagaId()); saga.advance(SagaState.PAYMENT_DONE); commands.send(new ConfirmOrder(saga.id(), saga.orderId())); }
@Transactional public void on(OrderConfirmed reply) { var saga = sagas.getById(reply.sagaId()); saga.advance(SagaState.COMPLETED); }
@Transactional public void on(PaymentFailed reply) { var saga = sagas.getById(reply.sagaId()); saga.advance(SagaState.COMPENSATING_INVENTORY); commands.send(new ReleaseInventory(saga.id(), saga.orderId())); }
@Transactional public void on(OrderRejected reply) { var saga = sagas.getById(reply.sagaId()); saga.advance(SagaState.COMPENSATING_PAYMENT); commands.send(new RefundPayment(saga.id(), saga.orderId(), saga.amount())); }
@Transactional public void on(PaymentRefunded reply) { var saga = sagas.getById(reply.sagaId()); saga.advance(SagaState.COMPENSATING_INVENTORY); commands.send(new ReleaseInventory(saga.id(), saga.orderId())); }
@Transactional public void on(InventoryReleased reply) { var saga = sagas.getById(reply.sagaId()); saga.advance(SagaState.FAILED); commands.send(new CancelOrder(saga.id(), saga.orderId())); }}sequenceDiagram
participant O as Orquestrador
participant Inv as Estoque
participant Pay as Pagamento
participant Ord as Pedido
O->>Inv: ReserveInventory
Inv-->>O: InventoryReserved
O->>Pay: ProcessPayment
Pay-->>O: PaymentFailed
Note over O: passo falhou -> compensar em ordem inversa
O->>Inv: ReleaseInventory (compensação)
Inv-->>O: InventoryReleased
O->>Ord: CancelOrder (compensação)
Ord-->>O: OrderCancelled
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Compensações não idempotentes: uma compensação reentregue pode estornar duas vezes. Torne-as idempotentes.
- Esquecer o passo pivô: tratar um passo irreversível como compensável leva a inconsistência quando a “compensação” não tem efeito real.
- Ler estado semi-committed sem contramedida: expor um pedido
PENDINGcomo se estivesse confirmado. Use flags de estado. - Coreografia em fluxos longos: o fluxo vira uma teia de eventos impossível de depurar. Migre para orquestração.
- Publicar evento sem Message Outbox: risco de dual write — o passo comita mas o evento se perde, travando a saga.
Relações
Seção intitulada “Relações”- Depende de Message Outbox para publicação confiável de Domain Events.
- Cada passo respeita a fronteira de um Agregado dentro de um Bounded Context.
- Coreografia é um caso de Event-Driven Architecture.
- Contrasta com transações locais e com a consistência forte assumida em muitos Microservices.
- Índice: Microservices Patterns.
Perguntas de revisão
Seção intitulada “Perguntas de revisão”- Qual a diferença entre uma transação compensatória e um rollback de banco?
Resposta
O rollback do banco desfaz fisicamente as mudanças de uma transação ainda não comitada. A compensação é uma nova transação de negócio que desfaz semanticamente um passo já comitado — estornar um pagamento, liberar estoque reservado, cancelar um pedido. Não volta o estado ao passado, cria um estado novo que anula o efeito.
- Por que uma SAGA não oferece isolamento e quais contramedidas existem?
Resposta
Porque cada passo comita localmente e fica visível antes de a saga inteira concluir, permitindo que outras transações leiam estado semi-committed (dirty read) ou o sobrescrevam (lost update). Contramedidas: flags de estado semântico (ex.: PENDING), locks semânticos, chaves de versão/otimista e reordenar para deixar o passo pivô o mais tarde possível.
- Quando escolher orquestração em vez de coreografia?
Resposta
Quando o fluxo é longo, tem ramificações condicionais, precisa de auditoria/observabilidade central ou envolve muitos serviços. A orquestração torna o fluxo explícito em uma máquina de estados. A coreografia brilha em fluxos curtos com times autônomos, mas o fluxo implícito por eventos vira difícil de rastrear quando cresce.
- O que é o passo pivô de uma saga?
Resposta
É o ponto de não-retorno: o último passo compensável. Antes dele, tudo pode ser desfeito por compensação; a partir dele, a saga só avança (passos retriable, executados com retry até sucesso). Identificar o pivô corretamente é essencial para não tentar compensar algo irreversível.