O Problema da Dupla Escrita
O dual write problem ocorre sempre que uma aplicação precisa fazer duas escritas em sistemas diferentes, normalmente um banco de dados e um broker de mensagens, e trata essas duas operações como uma só. Se a aplicação salva um pedido no banco e, em seguida, publica um evento no Azure Service Bus, cria-se uma janela de inconsistência. Se a aplicação falhar ou a rede falhar entre essas duas operações, o sistema termina em um estado inesperado: o dado existe mas o evento nunca foi publicado, ou o evento foi publicado mas o commit no banco nunca aconteceu.
⚠️ Atenção: um retry ingênuo não resolve o dual write problem sozinho. Se a aplicação falhar, pode ocorrer que o mesmo evento seja publicado duas vezes ou que o estado nunca seja reconciliado.
Por que Transações de Banco Não Resolvem Sozinhas
A tentativa mais comum de correção é envolver as duas operações em uma transação e torcer para que tudo funcione. Isso não funciona porque a transação do banco de dados só controla recursos que o próprio banco gerencia. Uma chamada HTTP ou AMQP para o broker não participa do protocolo de commit do banco de dados. Se a aplicação fizer INSERT no banco, chamar o SDK do Service Bus dentro do mesmo bloco try e depois dar COMMIT, ainda existem cenários de falha.
A Solução: Transactional Outbox Pattern
O Transactional Outbox Pattern resolve o dual write problem eliminando a necessidade de uma transação distribuída. Em vez de publicar diretamente no broker, a aplicação grava o dado de negócio e o evento pretendido em uma tabela de “caixa de saída” (outbox) dentro da mesma transação atômica do banco de dados. Como as duas gravações acontecem no mesmo banco e na mesma transação local, elas são atômicas por definição: ou as duas são persistidas, ou nenhuma é.
Código Fonte: O exemplo completo de Outbox, Inbox e Saga está disponível no repositório de exemplos do blog: BlogSamples/Messaging/EventDriven/
Se o CommitAsync for bem-sucedido, a garantia é de que o pedido e a intenção de publicar o evento existem juntos no banco. Se falhar, nenhum dos dois foi persistido. Não existe mais o cenário em que um foi salvo e o outro não.
Consumo dos Eventos: o Publisher Assíncrono
Salvar o evento na tabela outbox resolve a atomicidade, mas ainda falta o passo que efetivamente entrega a mensagem ao broker. Essa responsabilidade fica com um processo separado — geralmente um worker ou BackgroundService — que lê os registros pendentes da outbox e os publica no Azure Service Bus de forma assíncrona, marcando cada mensagem como publicada somente depois da confirmação do broker.
Caso Real: Auditoria Transacional em um Sistema de Gestão de Estoques
Um exemplo concreto desse padrão em produção é a outbox de auditoria de um sistema de gestão de estoque publica eventos no Azure Service Bus com semântica at-least-once. O publisher reivindica mensagens disponíveis por lease SQL, envia o envelope versionado para o tópico configurado e só marca a mensagem como Published depois da confirmação do broker.
Obrigações do Consumidor: Idempotência e Ordenação
Como a entrega é at-least-once, todo consumidor de uma outbox transacional precisa tratar duplicidade como parte normal do fluxo, não como exceção: idempotência por MessageId, PeekLock sem auto-complete e SessionId para ordenação por agregado. DLQ apenas após esgotar tentativas, registrando o erro de forma sanitizada sem payload sensível em log.
Retries Desacoplados entre Outbox e Consumidor
A outbox controla a publicação; o consumidor controla o próprio processamento. Misturar essas duas políticas de retry cria acoplamento desnecessário e dificulta o diagnóstico de qual lado está falhando.
ℹ️ Informação: o par Outbox (produtor) + Inbox (consumidor) é o que efetivamente resolve o dual write problem.