Transactional Outbox: como publicar eventos com segurança
Aprenda como o padrão Transactional Outbox evita a perda de eventos ao gravar dados de negócio e mensagens na mesma transação do banco.

Um serviço confirma um pedido no banco de dados e, logo depois, tenta publicar o evento para estoque, pagamento e entrega.
O primeiro passo funciona, mas a aplicação falha antes do segundo. Para o serviço de pedidos, a compra existe; para o restante do sistema, nada aconteceu.
O Transactional Outbox resolve essa lacuna ao gravar a alteração de negócio e o evento pendente na mesma transação local. Um processo separado lê a outbox e publica a mensagem posteriormente.
Assim, o sistema não depende de uma gravação no banco e de um envio ao broker acontecerem como se fossem uma única operação distribuída.
Neste guia, você entenderá o problema de dual write, o funcionamento do padrão, as diferenças entre polling e Change Data Capture, como lidar com duplicidade e ordenação e quais custos precisam ser considerados antes da implementação.
O que é Transactional Outbox?
Transactional Outbox é um padrão de consistência para aplicações que precisam modificar seus próprios dados e avisar outros componentes por mensagem ou evento.
Em vez de publicar diretamente durante a transação de negócio, a aplicação registra a mensagem em uma tabela ou coleção de saída — a outbox — no mesmo banco.
Depois do commit, um relay independente encontra os registros pendentes e os envia ao broker. Se o envio falhar, ele tenta novamente.
A documentação de padrões da AWS apresenta justamente essa solução para a inconsistência criada quando banco e mensageria são atualizados separadamente.
O padrão não torna o banco e o broker parte da mesma transação. Ele reduz o problema a uma transação ACID local, que o banco já sabe controlar, e transforma a publicação em um processo assíncrono recuperável.
O problema de dual write
Dual write acontece quando uma única decisão de negócio exige gravações em dois sistemas independentes.
Criar o pedido no PostgreSQL e publicar “PedidoCriado” em um broker são duas operações.
Cada uma pode ter sucesso ou falhar sem conhecer o resultado definitivo da outra.
| Banco | Publicação | Consequência |
|---|---|---|
| Sucesso | Sucesso | Estado e evento permanecem coerentes |
| Sucesso | Falha | O pedido existe, mas consumidores não ficam sabendo |
| Falha | Sucesso | Consumidores reagem a um pedido que não existe |
| Falha | Falha | Nenhuma alteração é confirmada |
A janela pode durar milissegundos e ainda assim importar. Queda do processo, timeout, indisponibilidade do broker ou perda da resposta podem interromper o fluxo exatamente entre os dois passos.
Em arquiteturas de microsserviços, esse tipo de falha parcial é parte normal do ambiente.
Por que trocar a ordem das operações não resolve
Publicar primeiro e gravar depois apenas inverte o risco. Se a mensagem for entregue e o banco rejeitar o commit, consumidores executarão ações baseadas em um fato que não foi confirmado.
Gravar primeiro e publicar depois evita esse cenário, mas permite que o evento seja perdido.
Tentar compensar com um bloco de código também não fecha todas as janelas.
A aplicação pode cair antes de registrar que precisa desfazer, ou a própria compensação pode falhar. Retentativas ajudam com falhas transitórias, mas, sem um registro durável da intenção, o sistema não sabe com segurança o que ficou pendente.
Como o Transactional Outbox funciona
- A aplicação inicia uma transação no banco local.
- Altera o estado do negócio, como criar um pedido.
- Insere na outbox um evento com identidade e payload.
- Confirma as duas gravações no mesmo commit.
- Um relay busca ou captura o novo registro.
- O relay publica a mensagem no broker.
- Após confirmação, marca o registro como publicado ou permite sua limpeza.
- Consumidores processam a mensagem de forma idempotente.
Se a transação falhar, não há pedido nem evento. Se ela concluir, ambos existem, mesmo que o broker esteja indisponível. A indisponibilidade atrasa a entrega, mas não apaga a intenção de publicar.
Estrutura de uma tabela outbox
| Campo | Finalidade |
|---|---|
| event_id | Identidade única usada para rastreio e deduplicação |
| aggregate_id | Identifica a entidade de negócio, como pedido ou conta |
| aggregate_type | Indica o tipo de entidade ou destino lógico |
| event_type | Nome estável do acontecimento |
| payload | Dados necessários ao consumidor |
| occurred_at | Momento em que o fato ocorreu |
| schema_version | Versão do contrato do evento |
| published_at | Controle opcional de publicação |
| attempts | Quantidade opcional de tentativas |
A estrutura varia conforme o relay. Uma solução baseada em polling costuma precisar de status, tentativas e bloqueio. Uma solução por captura do log pode tratar cada inserção como imutável e acompanhar a posição fora da tabela.
Em ambos os casos, o identificador do evento deve permanecer estável em todas as retentativas.
A atomicidade está na transação local
O ponto decisivo é inserir o registro da outbox usando a mesma conexão e a mesma transação que alteram os dados de negócio.
Chamar outro serviço, banco ou endpoint para gravar a outbox recria o dual write que o padrão deveria eliminar.
BEGIN; INSERT INTO pedidos (...); INSERT INTO outbox_events (event_id, aggregate_id, event_type, payload, occurred_at) VALUES (...); COMMIT;

A orientação da Microsoft para implementar outbox com Azure Cosmos DB segue o mesmo princípio: estado e evento são persistidos atomicamente antes de o evento ser encaminhado.
O mecanismo concreto muda entre bancos relacionais e NoSQL, mas a garantia necessária permanece.
O papel do relay de mensagens
O relay é o componente que transforma registros duráveis em mensagens publicadas.
Ele pode rodar no mesmo processo da aplicação, em um worker separado ou em uma plataforma de captura de mudanças. Separá-lo costuma melhorar escala, recuperação e isolamento.
Esse componente precisa continuar do ponto correto após reinício, limitar concorrência, repetir falhas transitórias e não deixar um registro defeituoso bloquear todos os seguintes.
Também deve distinguir “o broker recusou” de “o broker recebeu, mas a confirmação se perdeu”, pois o segundo caso pode gerar publicação repetida.
Opção 1: Polling Publisher
No polling, um worker consulta periodicamente registros não publicados. Ele reserva um lote, envia os eventos e atualiza o status.
É simples de entender e funciona mesmo quando não há acesso ao log transacional do banco.
- Vantagens: implementação direta, pouca infraestrutura adicional e controle explícito de lotes e retentativas.
- Cuidados: consultas frequentes, contenção entre workers, latência determinada pelo intervalo e necessidade de recuperar locks abandonados.
Use seleção com bloqueio compatível com o banco, como reservar linhas sem esperar pelas que outro worker já assumiu.
Processar cada lote em uma transação longa pode manter locks por tempo demais; normalmente a reserva é curta e o envio acontece fora dela, aceitando que uma confirmação perdida possa causar repetição.
Opção 2: Change Data Capture
Com Change Data Capture, um conector observa o log de transações e envia as inserções da outbox ao pipeline. A aplicação apenas grava o evento; não precisa consultar a tabela repetidamente.
O Outbox Event Router do Debezium captura mudanças na tabela e transforma cada registro em mensagem roteada.
Essa abordagem pode oferecer baixa latência e boa escala, mas exige operar conectores, permissões de leitura do log, offsets, compatibilidade de esquema e recuperação do pipeline.

Polling ou CDC: como escolher?
| Critério | Polling | CDC |
|---|---|---|
| Complexidade inicial | Menor | Maior |
| Latência | Depende do intervalo | Geralmente próxima do tempo real |
| Carga no banco | Consultas periódicas | Leitura do log transacional |
| Infraestrutura | Worker e banco | Conector e pipeline de CDC |
| Controle da aplicação | Alto | Parte fica na plataforma |
| Escala operacional | Exige particionar e coordenar workers | Depende da capacidade do conector e do log |
Polling costuma ser suficiente para cargas moderadas e equipes que valorizam simplicidade. CDC faz sentido quando a organização já domina a tecnologia, precisa de menor latência ou publica grande volume.
A escolha deve considerar operação, não apenas velocidade em um teste local.
A publicação ainda pode ser duplicada
O relay pode publicar uma mensagem, falhar antes de marcar o registro e publicá-la novamente após reiniciar. Por isso, Transactional Outbox evita perda entre estado e intenção de publicação, mas não oferece exatamente uma entrega ponta a ponta.
Consumidores devem registrar o event_id já processado ou realizar uma alteração naturalmente idempotente.
A identidade precisa ser verificada na mesma transação do efeito local do consumidor.
Caso contrário, ele pode marcar a mensagem como vista e falhar antes de aplicar o negócio, ou aplicar o negócio e falhar antes de registrar a deduplicação.
Como preservar a ordem dos eventos
Um pedido pode gerar “Criado”, “Pago” e “Cancelado”. Publicá-los fora de ordem deixa consumidores em estado incorreto. Ordenação global geralmente custa paralelismo; o requisito comum é preservar a sequência por agregado.
- grave aggregate_id em cada evento;
- use essa identidade como chave de particionamento no broker;
- mantenha uma sequência ou versão por agregado quando necessário;
- processe eventos do mesmo agregado de forma serial ou com controle otimista;
- defina como o consumidor reage a uma versão atrasada.
O commit do banco determina uma ordem local, mas concorrência e relays paralelos podem alterá-la na publicação.
A garantia precisa ser planejada de ponta a ponta, dentro do escopo realmente necessário.
Falhas, retentativas e recuperação
O relay deve repetir falhas transitórias com espera progressiva e variação aleatória, evitando pressionar um broker indisponível. Limites de concorrência e circuit breaker podem proteger tanto banco quanto mensageria durante incidentes.
Um evento que falha continuamente precisa ir para quarentena ou receber um estado de erro investigável.
Ignorá-lo silenciosamente quebra a promessa do padrão; bloquear toda a fila por causa dele também pode paralisar o sistema.
Defina reprocessamento manual, alertas e critérios de descarte autorizados.
Limpeza e retenção da outbox
Uma tabela que cresce sem limite aumenta índices, backups e tempo de consulta. Registros confirmados podem ser removidos ou arquivados depois de uma janela compatível com auditoria e recuperação.
Faça a limpeza em lotes pequenos para não criar locks longos nem picos de I/O.
Não apague apenas por idade se ainda houver eventos pendentes. Em CDC, confirme que o conector avançou antes de reduzir a retenção do log ou da tabela.
Políticas de backup também devem preservar a relação entre dados de negócio e eventos durante restaurações.
Contrato e evolução dos eventos
A outbox transporta um contrato que pode sobreviver a várias versões da aplicação. Use nomes estáveis, versão explícita e campos suficientes para o consumidor agir sem depender de detalhes internos do produtor.
Evite serializar diretamente uma entidade do ORM, pois uma mudança de banco pode alterar o evento sem intenção.
Prefira mudanças compatíveis, como adicionar campos opcionais. Quando uma alteração for incompatível, publique nova versão e mantenha consumidores antigos durante a migração.
Informações de correlação e causa ajudam a reconstruir a jornada entre eventos.
Observabilidade do fluxo
- quantidade de eventos pendentes e idade do mais antigo;
- tempo entre commit e publicação;
- taxa de publicação, retentativas e erros permanentes;
- tamanho da tabela e velocidade de limpeza;
- posição e atraso do conector CDC;
- duplicidades detectadas pelos consumidores;
- eventos em quarentena e tempo de resolução.
Inclua event_id, aggregate_id, correlation_id e causation_id em logs estruturados, sem registrar payload sensível.
Com OpenTelemetry, traces podem relacionar a transação original, o relay e o consumidor. Combine isso com uma estratégia de observabilidade e monitoramento que alerte pela idade da fila, não somente por erros do processo.
Segurança e privacidade
O payload da outbox fica no banco e atravessa infraestrutura de mensageria. Inclua apenas os dados necessários, aplique controles de acesso e criptografia compatíveis com a classificação da informação e defina retenção.
Dados pessoais duplicados em eventos tornam correção e exclusão mais difíceis.
Proteja o relay com permissões mínimas: leitura e atualização da outbox, além de publicação apenas nos destinos autorizados.
Em integrações externas, assinatura e validação de webhooks continuam necessárias; o padrão garante a intenção de enviar, não a identidade de quem recebe.
Principais benefícios
- Consistência: estado de negócio e intenção de publicar são confirmados juntos.
- Recuperação: indisponibilidade do broker gera atraso, não perda silenciosa.
- Desacoplamento: a transação local não precisa aguardar consumidores.
- Auditoria: eventos pendentes e publicados podem ser rastreados.
- Compatibilidade: funciona com diferentes bancos e brokers.
- Escala gradual: relay pode evoluir de polling simples para CDC quando necessário.
Limitações e custos
- consistência entre serviços continua eventual;
- mensagens podem ser publicadas mais de uma vez;
- consumidores precisam ser idempotentes;
- ordenação exige desenho por agregado ou partição;
- a tabela requer índices, retenção e limpeza;
- polling adiciona consultas, enquanto CDC adiciona infraestrutura;
- contratos de evento precisam de governança;
- restaurações e migrações exigem cuidado com offsets e replay.
Transactional Outbox também não substitui uma saga. Outbox publica de forma confiável o evento que inicia ou continua um fluxo; saga coordena várias transações locais e possíveis compensações.
Os padrões resolvem problemas relacionados, mas diferentes.
Quando usar Transactional Outbox
Use quando uma alteração confirmada no banco precisa inevitavelmente gerar uma mensagem e a perda dessa notificação criaria inconsistência. Pedidos, pagamentos, estoque, faturamento, cadastros e projeções de leitura são exemplos frequentes.
O padrão pode ser excessivo quando a notificação é apenas auxiliar e perder uma ocorrência é aceitável, quando tudo acontece no mesmo banco sem mensageria ou quando a plataforma já fornece uma garantia atômica equivalente.
Comece pelo requisito de consistência, não pela popularidade do nome.
Como implementar com segurança
- Defina o fato de negócio que merece ser publicado.
- Crie identidade estável e contrato versionado para o evento.
- Grave dados e outbox na mesma transação local.
- Escolha polling ou CDC conforme carga e capacidade operacional.
- Projete o relay para retentativa, concorrência e recuperação.
- Particione por agregado quando houver requisito de ordem.
- Torne consumidores idempotentes usando event_id.
- Defina quarentena, reprocessamento e política de limpeza.
- Instrumente atraso, backlog, erros e duplicidade.
- Teste quedas em cada fronteira antes de liberar o fluxo.
Nos testes, derrube a aplicação antes e depois do commit, interrompa o relay antes e depois da confirmação do broker, indisponibilize a mensageria e repita a entrega ao consumidor.
O resultado esperado não é ausência de falhas, mas recuperação sem perder o evento nem duplicar o efeito de negócio.
Erros comuns
- Usar outra conexão para a outbox: as gravações deixam de ser atômicas.
- Publicar dentro da transação do banco: mantém locks enquanto depende da rede e ainda não elimina todas as incertezas.
- Apagar antes da confirmação: uma falha pode perder a mensagem.
- Presumir entrega única: uma confirmação perdida pode produzir publicação duplicada.
- Não versionar o payload: deploys passam a quebrar consumidores antigos.
- Ignorar eventos presos: a tabela cresce enquanto o negócio fica inconsistente.
- Consultar sem índice: o polling degrada conforme o volume aumenta.
- Reter para sempre: outbox não deve virar arquivo histórico por acidente.
Perguntas frequentes
Transactional Outbox garante exactly once?
Não. O padrão evita perder a intenção de publicar, mas o relay pode enviar a mesma mensagem novamente. Consumidores idempotentes fazem o efeito parecer único dentro da fronteira definida.
O padrão precisa de Kafka?
Não. A outbox pode publicar para diferentes brokers ou serviços. Kafka é uma opção comum, especialmente com CDC, mas não faz parte da definição do padrão.
A tabela outbox substitui uma fila?
Não completamente. Ela mantém a intenção de publicar junto ao estado local. O broker continua oferecendo distribuição, retenção, grupos de consumidores e escala de mensageria.
Transactional Outbox é Event Sourcing?
Não. Na outbox, o estado atual continua sendo persistido normalmente e eventos são registrados para integração. Em Event Sourcing, a sequência de eventos é a fonte principal para reconstruir o estado.
É melhor implementar com polling ou CDC?
Depende da escala e da maturidade operacional. Polling é mais simples; CDC tende a oferecer menor latência e menos consultas, mas adiciona conectores e gestão de offsets.
Por quanto tempo os eventos devem ficar na outbox?
Até existir confirmação suficiente de publicação e passar a janela necessária para auditoria e recuperação. O prazo varia conforme risco, volume e requisitos regulatórios.
Qual é a relação entre Transactional Outbox e Saga?
A outbox publica com segurança os eventos de cada transação local. A saga usa eventos ou comandos para avançar um processo entre serviços e executar compensações quando necessário.
Conclusão
Transactional Outbox elimina a perigosa lacuna entre confirmar dados e registrar que uma mensagem precisa ser publicada.
A alteração de negócio e o evento entram na mesma transação local; um relay recuperável cuida da entrega assíncrona por polling ou CDC.
A confiabilidade depende do conjunto: identidade estável, consumidores idempotentes, ordenação quando necessária, retentativas controladas, limpeza e observabilidade.
Com essa base, uma indisponibilidade temporária deixa de causar perda silenciosa.
O próximo passo é entender como eventos confiáveis participam de transações distribuídas mais longas — exatamente o problema tratado pelo padrão Saga.