1. Vamos seguir uma compra online

Imagine que você apertou “Comprar”. Parece uma ação só, mas a loja talvez precise reservar o item no estoque, autorizar o pagamento e solicitar a entrega. Em um sistema pequeno, tudo pode acontecer dentro de uma aplicação e uma transação de banco. Em uma plataforma maior, estoque, pagamentos e logística podem estar em serviços separados, cada qual dono de seus dados.

Agora imagine que o pagamento foi autorizado, mas o serviço de estoque ficou indisponível. A compra não pode simplesmente desaparecer. Ou o pedido será concluído quando o estoque voltar, ou o pagamento terá de ser estornado. Essa é a pergunta central: como manter um processo de negócio coerente quando não existe uma única transação cobrindo todos os serviços?

Antes de apresentar os padrões, guarde três ideias simples: uma falha parcial é normal; uma mensagem pode chegar mais de uma vez; e serviços diferentes não compartilham automaticamente uma única transação. Saga, Outbox e Inbox ajudam a lidar com essas realidades, cada um num ponto específico.

2. Por que uma transação de banco só não resolve?

Uma transação local pode garantir que várias gravações no mesmo banco ocorram juntas: todas confirmam ou todas são desfeitas. Mas, se o serviço de pedidos usa um banco, pagamentos usam outro e a transportadora é uma API externa, tentar abranger tudo em uma transação distribuída adiciona dependência operacional, latência e pontos de falha.

Em muitos sistemas, é melhor aceitar que o processo terá etapas confirmadas em momentos distintos e definir como avançar ou compensar. Isso é consistência eventual: por algum intervalo, serviços podem observar estados diferentes, mas o sistema converge para um resultado conhecido. Não significa “dados podem ficar errados para sempre”; significa que o modelo explicita a janela e as ações para resolvê-la.

Por exemplo, depois de receber uma solicitação, a tela pode mostrar “Pagamento em processamento”. Não precisa fingir que o pagamento e o estoque mudaram ao mesmo nanossegundo. O importante é comunicar o estado real, definir limites de tempo e oferecer um desfecho para sucesso, recusa e falha técnica.

3. O que é uma Saga?

Uma Saga é uma sequência de transações locais que, juntas, realizam uma operação de negócio distribuída. Cada etapa confirma seus dados no serviço que a possui e, quando uma etapa posterior impede a conclusão, o processo pode executar ações compensatórias para aproximar o negócio de um estado aceitável.

Para a compra: criar pedido pendente; reservar estoque; autorizar pagamento; confirmar pedido. Se o pagamento for recusado após a reserva, a compensação pode liberar o estoque. Não é uma máquina do tempo que desfaz todas as consequências: é uma nova ação de negócio. Um pagamento já capturado pode exigir reembolso, que talvez leve horas e tenha taxas; isso não é igual a apagar a autorização original.

Passos e compensações precisam de significado

Uma tabela simples ajuda a começar o desenho. Para cada passo, pergunte qual é o resultado esperado, como saber que terminou e o que fazer se o processo não puder continuar.

PassoAção normalSe uma etapa posterior falhar
1. PedidoCriar pedido pendenteMarcar como cancelado ou falho
2. EstoqueReservar itens por tempo limitadoLiberar a reserva
3. PagamentoAutorizar o valorEstornar ou cancelar autorização
4. ConfirmaçãoConfirmar pedido e iniciar entregaIntervir se a etapa não puder ser recuperada

Compensações nem sempre são simétricas. Se um pacote já foi enviado, “desenviar” não existe: pode ser necessário solicitar devolução ou encaminhar para atendimento. Por isso, uma Saga é um processo de negócio explícito, não simplesmente um rollback automático distribuído.

4. Orquestração ou coreografia?

Há duas formas comuns de coordenar os passos. Na orquestração, um coordenador conhece o fluxo: solicita reserva, aguarda o resultado, pede autorização e decide a próxima ação. É parecido com uma pessoa organizando uma mudança e confirmando cada serviço contratado.

Na coreografia, cada serviço reage a eventos: pedido criado causa uma tentativa de reserva; estoque reservado publica outro evento; pagamentos reage a ele. Não há um maestro central para cada decisão, e o fluxo emerge das reações combinadas. Isso pode reduzir coordenação central, mas torna o caminho completo mais difícil de enxergar.

Nenhuma estratégia é sempre melhor. Orquestração costuma facilitar fluxos longos, com muitas decisões, prazos e compensações. Coreografia pode funcionar bem para reações pequenas e desacopladas, desde que o time mantenha um mapa claro de eventos e responsabilidades. Se cada serviço publica eventos que disparam cadeias invisíveis, o sistema pode ficar mais acoplado, não menos.

Uma pergunta prática ajuda: quando um pedido fica parado, alguém consegue descobrir rapidamente qual passo está pendente, quantas tentativas ocorreram e qual ação manual é possível? Se a resposta depende de rastrear dezenas de consumidores, talvez o fluxo precise de um coordenador explícito ou de melhores ferramentas de observabilidade.

5. Outbox: não perder a mensagem entre banco e broker

Considere o serviço de pedidos. Ele grava o pedido no banco e depois publica PedidoCriado no broker. O que acontece se o banco confirmar a gravação e o processo cair antes da publicação? O pedido existe, mas estoque e pagamento nunca ficam sabendo.

Publicar primeiro também não resolve: o evento pode chegar aos consumidores e, em seguida, a gravação do pedido falha. Agora o restante do sistema acredita numa compra que não foi persistida. São duas operações em sistemas diferentes; sem uma transação compartilhada, sempre há uma janela entre elas.

O padrão Transactional Outbox reduz essa janela gravando, na mesma transação local, tanto a alteração de negócio quanto uma mensagem pendente numa tabela outbox. Um processo separado lê as mensagens pendentes e as publica no broker. Se a aplicação cair antes do commit, nem o pedido nem o evento ficam gravados. Se cair depois do commit, a mensagem continua na outbox para ser publicada mais tarde.

BEGIN TRANSACTION;

INSERT INTO Orders (Id, Status, Total)
VALUES (@orderId, 'Pending', @total);

INSERT INTO OutboxMessages (Id, Type, Payload, OccurredAt)
VALUES (@messageId, 'OrderCreated', @payload, CURRENT_TIMESTAMP);

COMMIT;

Um publicador lê mensagens ainda não enviadas, publica e registra que foram entregues. Como pode cair depois de publicar, mas antes de atualizar a linha, é possível que publique a mesma mensagem novamente. Por isso, Outbox normalmente garante entrega pelo menos uma vez, não exatamente uma única vez. Consumidores precisam tolerar duplicatas.

A outbox também não faz o broker e o banco virarem uma única transação. Ela torna durável a intenção de publicar e permite repetição após falha. Ainda é necessário cuidar de ordenação, tamanho e limpeza da tabela, tentativas, mensagens inválidas e monitoramento do atraso entre criação e publicação.

6. Inbox: proteger o consumidor de mensagens repetidas

Agora o serviço de estoque recebe OrderCreated. O broker pode entregar novamente se não recebeu confirmação, ou o publicador da outbox pode repetir após uma falha ambígua. Se o consumidor reservar o mesmo item duas vezes, temos um problema.

O padrão Inbox registra identificadores de mensagens já processadas. Antes de aplicar o efeito, o consumidor verifica se o ID já foi visto. A gravação do ID e a alteração de negócio devem ocorrer na mesma transação local, com uma restrição única sobre o identificador.

BEGIN TRANSACTION;

INSERT INTO InboxMessages (MessageId, ProcessedAt)
VALUES (@messageId, CURRENT_TIMESTAMP);

UPDATE Inventory
SET Reserved = Reserved + @quantity
WHERE ProductId = @productId;

COMMIT;

Se a inserção no Inbox encontrar um ID duplicado, a mensagem já foi tratada; o consumidor pode confirmar a entrega sem aplicar novamente o efeito. Se o processo cair antes do commit, tanto o registro de inbox quanto a alteração são revertidos, permitindo uma nova tentativa segura.

Inbox não é uma lista infinita de mensagens sem política de retenção. Defina por quanto tempo IDs precisam ser lembrados, considerando o tempo máximo de redelivery, replay e recuperação. Para efeitos externos que não compartilham a transação do consumidor — por exemplo, cobrar um provedor de pagamento — a idempotência também precisa ser suportada pela própria integração, usando uma chave idempotente reconhecida pelo provedor.

7. O fluxo completo, passo a passo

  1. Uma requisição pede a criação de um pedido. O serviço de pedidos grava o estado pendente e OrderCreated na Outbox, na mesma transação.
  2. Um publicador envia o evento ao broker. Se não consegue confirmar que publicou, tenta de novo; duplicatas são possíveis.
  3. O serviço de estoque recebe a mensagem. Em uma transação local, verifica a Inbox, registra o ID e cria a reserva. Depois publica StockReserved usando sua própria Outbox.
  4. O coordenador da Saga recebe o resultado e solicita a autorização do pagamento, com identificador idempotente associado ao pedido.
  5. Se o pagamento for aprovado, a Saga solicita a confirmação do pedido. Se for recusado, solicita a liberação da reserva e encerra como pagamento recusado.
  6. Se um serviço estiver indisponível, a mensagem ou o comando é tentado novamente conforme uma política limitada. Se o processo exceder os limites, é sinalizado para análise e possível intervenção.

Repare que cada serviço grava seus próprios dados e publica suas próprias mensagens de forma durável. A Saga descreve a progressão e as compensações; Outbox fecha a lacuna entre gravação local e publicação; Inbox torna o consumo repetível sem duplicar o efeito local. Eles resolvem problemas relacionados, mas não são nomes diferentes para a mesma solução.

Em uma aplicação .NET, esses padrões podem ser implementados com tabelas e processos de background, ou por bibliotecas de mensageria que oferecem recursos equivalentes. A biblioteca ajuda com transporte e execução, mas a equipe ainda precisa definir o estado de negócio, as compensações, as chaves de idempotência e como investigar uma Saga parada.

8. Falhas, idempotência e operação

Idempotência: repetir sem repetir o efeito

Uma operação é idempotente quando repetir a mesma solicitação produz o mesmo resultado final que executá-la uma vez. “Definir pedido como confirmado” pode ser idempotente; “somar uma unidade reservada” não é automaticamente. Um ID estável da mensagem ou uma chave de operação permite reconhecer que uma tentativa já foi aplicada.

Pense na idempotência como o recibo de uma entrega: se a mensagem chegar de novo, o serviço consulta o recibo e responde “já cuidei disso”, em vez de executar a cobrança outra vez. A chave deve representar a intenção estável, não ser gerada novamente em cada retry.

Retries, timeouts e mensagens problemáticas

Retry sem limite pode piorar uma indisponibilidade, criando uma tempestade de chamadas. Defina número ou duração máxima, atraso progressivo, variação aleatória quando apropriado, e quais erros podem ser repetidos. Uma validação inválida normalmente não melhora com retry; um timeout pode indicar sucesso desconhecido, então a operação precisa ser idempotente antes de repetir.

Mensagens que falham continuamente precisam sair do ciclo normal e ficar visíveis para investigação, por exemplo numa dead-letter queue. Reprocessá-las exige cuidado: o estado do domínio pode ter mudado desde a primeira tentativa. Disponibilize correlação por ID de pedido e Saga, histórico de passos, motivo da falha, número de tentativas e uma ação de recuperação autorizada.

Ordenação e concorrência

Filas podem entregar eventos fora de ordem, especialmente quando há partições, retries ou consumidores paralelos. Se uma etapa depende de outra, inclua versão ou sequência e rejeite/transicione mensagens incompatíveis de forma explícita. Restrições de banco continuam essenciais: duas instâncias podem verificar simultaneamente que não existe uma reserva e tentar criá-la.

9. Quando usar — e quando não usar

Considere Saga quando um fluxo de negócio precisa atravessar serviços que possuem dados independentes e não há uma transação global apropriada. Outbox é útil quando um serviço grava estado e precisa publicar uma mensagem sem perder a intenção numa falha entre banco e broker. Inbox ajuda quando redelivery é possível e um efeito duplicado seria danoso.

Não adote esses padrões só porque a arquitetura usa microsserviços. Se tudo acontece dentro de um serviço e uma transação local atende aos requisitos, ela pode ser mais simples e mais forte. Outbox e Inbox trazem tabelas, processos, retries, retenção e observabilidade para operar. Distribuir uma operação que poderia permanecer local cria novos modos de falha.

Comece com perguntas do negócio: o que o usuário deve ver enquanto aguarda? Quanto tempo a reserva dura? Uma cobrança pode ser estornada? Quem resolve um processo parado? Quais efeitos podem ser repetidos? Quando essas respostas são claras, escolha uma coreografia ou orquestração e implemente somente os mecanismos necessários.

Esses padrões combinam bem com limites explícitos entre domínio, casos de uso e infraestrutura. Para aprofundar esse desenho, leia Clean Architecture e a Dependency Rule, DDD e modelagem de domínio e SOLID na prática.