Pular para o conteúdo

SAGA

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.

  1. Modele a operação como N passos Tᵢ, cada um uma transação local.
  2. Para cada passo, defina (quando aplicável) uma compensação Cᵢ que desfaz semanticamente Tᵢ.
  3. Execute T₁ … Tₙ em ordem. Se Tₖ falhar, execute Cₖ₋₁ … 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).

  • 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.

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.

  • 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 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ô.

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
}
@Entity
public 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
@RequiredArgsConstructor
public 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
  • 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 PENDING como 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.
  1. 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.

  1. 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.

  1. 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.

  1. 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.