Como manter a consistência de negócio entre microsserviços quando transações ACID tradicionais deixam de ser uma opção.
No desenvolvimento de software corporativo moderno, a transição de monólitos para arquiteturas distribuídas e microsserviços traz ganhos inegáveis de escalabilidade e autonomia de times. Porém, essa mudança cobra um preço alto em um dos pilares mais fundamentais da engenharia de software: a consistência de dados.
No monólito tradicional com banco relacional único, garantir que um pedido só fosse criado se o estoque fosse reservado e o pagamento aprovado era trivial: bastava abrir um bloco BEGIN TRANSACTION, rodar os inserts/updates e executar um COMMIT. Se qualquer linha falhasse, o banco revertia tudo de forma atômica.
Em um ecossistema distribuído onde cada microsserviço possui seu próprio banco de dados isolado (Database-per-Service), transações ACID locais não cruzam as fronteiras de rede. Protocolos clássicos como Two-Phase Commit (2PC) são lentos, bloqueantes e criam pontos únicos de falha, sendo impraticáveis em escala de alta performance. É exatamente aqui que o Padrão Saga se torna indispensável.
O Problema: O Falso Sucesso e o Estado Inconsistente
Vamos a um cenário clássico de e-commerce e logística: o fluxo de finalização de um pedido (Checkout).
O fluxo de negócio requer três passos sequenciais em serviços distintos:
- Order Service: Cria o pedido com status
PENDENTE. - Payment Service: Efetua a cobrança no cartão do cliente.
- Inventory Service: Baixa as unidades físicas do produto no centro de distribuição.
O Cenário do Desastre:
O Order Service cria o pedido com sucesso. O Payment Service aprova a cobrança de R$ 500,00 no cartão do usuário. Porém, ao chamar o Inventory Service, descobrimos que o último lote do produto acabou de ser vendido em outra sessão, ou o serviço de inventário sofre um timeout de rede indevido.
O resultado é catastrófico: o cliente foi cobrado, o pedido ficou preso no limbo e o estoque não existe. Como não há um rollback automático entre bancos de dados diferentes, o sistema entrou em estado de inconsistência grave de negócio.
A Solução: O Conceito do Padrão Saga
Proposto originalmente em 1987 por Hector Garcia-Molina e Kenneth Salem, o padrão Saga decompõe uma transação distribuída de longa duração em uma sequência linear de transações locais.
Cada transação local atualiza o banco de dados do respectivo serviço e emite uma mensagem ou evento que dispara o próximo passo. Se qualquer transação da cadeia falhar, a Saga executa uma série de Transações Compensatórias (Compensating Transactions) para desfazer retroativamente as alterações já consolidadas.
Atenção ao detalhe arquitetural: Uma transação compensatória não é um
ROLLBACKdo banco de dados. Ela é uma nova transação de negócio que reverte o efeito semântico da etapa anterior. Se a etapa anterior foi debitar a conta, a compensação é estornar o crédito; se foi reservar um item, a compensação é cancelar a reserva.
As Duas Estratégias de Implementação
Existem dois modelos principais para coordenar as transações de uma Saga: Coreografia e Orquestração. Escolher o errado é uma das principais fontes de débito técnico em microsserviços.
1. Coreografia (Baseada em Eventos Descentralizados)
Na coreografia, não existe um coordenador central. Cada microsserviço ouve eventos dos serviços vizinhos, executa sua transação local e publica seus próprios eventos de sucesso ou falha no broker (Kafka, RabbitMQ, etc.).
- Vantagens: Baixo acoplamento inicial, ideal para fluxos simples com poucos serviços (2 a 3 etapas).
- Desvantagens: O fluxo de negócio fica difuso pelo código; difícil rastreabilidade, risco de dependências cíclicas e pesadelo de observabilidade à medida que o sistema cresce.
2. Orquestração (Coordenador Central de Estado)
Na orquestração, definimos um componente explícito — o Saga Orchestrator (ou Máquina de Estados) — que gerencia ativamente o ciclo de vida da transação. Ele envia comandos direcionados a cada participante e escuta suas respostas para determinar o próximo passo ou disparar a cadeia de compensações.
- Vantagens: Centralização da lógica e regras de negócio; excelente visibilidade do estado da transação; tratamento de erros e compensações explícito e auditável.
- Desvantagens: Risco de concentrar lógica excessiva no orquestrador se o time não delimitar bem as responsabilidades de cada serviço.
Onde isso vive na arquitetura? Implementando com C# (.NET)
No ecossistema .NET, bibliotecas como MassTransit com Automatonymous / State Machine Sagas são amplamente consolidadas para implementar orquestração com persistência de estado (em Redis, PostgreSQL ou MongoDB).
Abaixo, veja a estrutura conceitual de como definimos uma máquina de estados para a Saga do Pedido:
public class OrderSagaState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public Guid OrderId { get; set; }
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
}
public class OrderStateMachine : MassTransitStateMachine<OrderSagaState>
{
public State ProcessingPayment { get; private set; }
public State ReservingInventory { get; private set; }
public State Completed { get; private set; }
public State Faulted { get; private set; }
public Event<IOrderSubmitted> OrderSubmitted { get; private set; }
public Event<IPaymentApproved> PaymentApproved { get; private set; }
public Event<IPaymentRejected> PaymentRejected { get; private set; }
public Event<IInventoryReserved> InventoryReserved { get; private set; }
public Event<IInventoryOutOfStock> InventoryOutOfStock { get; private set; }
public OrderStateMachine()
{
InstanceState(x => x.CurrentState);
// Início da Saga
Initially(
When(OrderSubmitted)
.Then(context => {
context.Saga.OrderId = context.Message.OrderId;
context.Saga.TotalAmount = context.Message.TotalAmount;
})
.Publish(context => new ProcessPaymentCommand(context.Saga.OrderId, context.Saga.TotalAmount))
.TransitionTo(ProcessingPayment)
);
// Fluxo de Pagamento Aprovado -> Tenta Reservar Estoque
During(ProcessingPayment,
When(PaymentApproved)
.Publish(context => new ReserveInventoryCommand(context.Saga.OrderId))
.TransitionTo(ReservingInventory),
When(PaymentRejected)
.Publish(context => new CancelOrderCommand(context.Saga.OrderId, "Pagamento Recusado"))
.TransitionTo(Faulted)
);
// Fluxo de Falha no Estoque -> Dispara Transação Compensatória no Pagamento!
During(ReservingInventory,
When(InventoryReserved)
.Publish(context => new CompleteOrderCommand(context.Saga.OrderId))
.TransitionTo(Completed),
When(InventoryOutOfStock)
// A Compensação: estorna o pagamento previamente efetuado
.Publish(context => new RefundPaymentCommand(context.Saga.OrderId, context.Saga.TotalAmount))
.Publish(context => new CancelOrderCommand(context.Saga.OrderId, "Estoque Insuficiente"))
.TransitionTo(Faulted)
);
}
}
As Regras de Ouro para Sagas em Produção
- Idempotência Obrigatória em Todos os Participantes: Em redes instáveis, mensagens de comando ou compensação podem ser reenviadas. Todo handler de comando ou compensação deve ser estritamente idempotente.
- Isolamento Semântico: O modelo Saga garante Consistência Eventual, mas não oferece Isolamento (o "I" do ACID). Outras transações podem ler dados intermediários (dirty reads semânticos). Trate isso com status explícitos como
PENDENTE_CONFIRMACAO. - Correlation ID de Ponta a Ponta: Toda mensagem trafegada deve carregar o mesmo
CorrelationIdpara viabilizar rastreamento em logs distribuídos e OpenTelemetry.
Conclusão
Adotar microsserviços sem um modelo claro de transações distribuídas é uma aposta arriscada que invariavelmente estoura na reconciliação financeira ou no suporte ao cliente. O padrão Saga não elimina a complexidade inerente da rede, mas a torna explícita, auditável e recuperável.
Se o seu fluxo possui poucos serviços independentes, a coreografia pode servir inicialmente. Mas quando o negócio exige garantias reais, observabilidade cristalina e compensações complexas, a orquestração com máquina de estados é o investimento arquitetural que protege a consistência da sua operação em escala.