Todo mundo que integra banco de dados com Kafka encontra a mesma pergunta em algum momento:
o que acontece se eu gravar no banco, mas falhar ao publicar o evento?
No começo, a resposta parece ser apenas "tenta de novo".
Mas esse "tenta de novo" esconde uma das falhas mais perigosas em sistemas orientados a eventos: o estado muda em um lugar, mas o fato não chega aos outros.
O pedido foi criado no banco.
O evento OrderCreated não foi publicado.
Para o serviço dono do banco, o pedido existe.
Para os consumidores que dependem do Kafka, ele nunca aconteceu.
Esse é o tipo de inconsistência que não aparece como exceção bonita no log.
Ela aparece depois, como atendimento manual, pedido travado, estoque não reservado, cobrança ausente, projeção desatualizada ou relatório errado.
O Outbox Pattern existe para atacar exatamente essa fronteira.
Não para transformar banco e Kafka em uma única transação distribuída.
Mas para garantir que a decisão de negócio e a intenção de publicar um evento sejam persistidas juntas.
O problema não é publicar evento
Publicar um evento no Kafka é simples.
Gravar uma linha no banco também.
O problema nasce quando as duas coisas precisam representar a mesma mudança de estado.
Imagine um fluxo comum:
- validar uma regra de negócio;
- gravar o pedido no banco;
- publicar
OrderCreatedno Kafka; - retornar sucesso para o cliente.
Esse fluxo parece razoável até a produção lembrar que sistemas falham entre uma linha e outra.
Se o banco grava e a publicação falha, o estado local mudou, mas o restante do sistema não soube.
Se o evento é publicado antes e o banco falha depois, os consumidores podem reagir a um fato que nunca foi confirmado.
Se a aplicação cai entre as duas operações, talvez ninguém saiba com certeza qual lado aconteceu.
Essa é a armadilha:
duas operações podem ser corretas isoladamente e ainda assim formar uma fronteira inconsistente quando precisam acontecer juntas.
Transação local não protege recurso externo
Dentro do banco, a transação local funciona muito bem.
Você altera tabelas, valida restrições, confirma tudo no commit ou descarta no rollback.
O banco sabe controlar seus próprios recursos.
Kafka está fora dessa fronteira.
Quando a aplicação abre uma transação no banco e depois chama o producer do Kafka, ela está coordenando dois mundos diferentes no braço.
O banco não sabe se o Kafka publicou.
O Kafka não sabe se o banco commitou.
A aplicação fica no meio tentando fazer as duas coisas parecerem atômicas.
Esse é o ponto onde muita arquitetura tenta resolver com ordem de chamada:
- "primeiro grava, depois publica";
- "primeiro publica, depois grava";
- "se falhar, faz retry";
- "se der erro, manda para uma fila";
- "se cair, o monitor detecta".
Essas decisões ajudam em alguns cenários, mas não fecham a fronteira principal.
Enquanto a mudança de estado e a intenção de publicar o evento estiverem em recursos diferentes, ainda existe uma janela em que uma parte pode acontecer sem a outra.
O que o Outbox muda
O Outbox Pattern muda a pergunta.
Em vez de tentar gravar no banco e publicar no Kafka como se fossem uma única operação, ele coloca a intenção de publicação dentro da mesma transação do banco.
Na prática, o serviço grava duas coisas juntas:
- a alteração de negócio;
- um registro em uma tabela de outbox descrevendo o evento que precisa ser publicado.
Exemplo simplificado:
BEGIN
INSERT INTO orders (...)
INSERT INTO outbox_events (
event_id,
aggregate_type,
aggregate_id,
event_type,
payload,
created_at,
published_at
)
COMMIT
Se a transação confirmar, o pedido existe e a intenção de publicar OrderCreated também existe.
Se a transação falhar, nenhum dos dois existe.
Essa é a garantia central do padrão.
O evento ainda não foi necessariamente publicado no Kafka.
Mas agora ele está registrado em um lugar durável, transacional e consultável.
Depois disso, outro processo publica os registros pendentes da outbox no Kafka.
Esse processo pode ser um worker, um scheduler, um conector baseado em CDC ou uma combinação bem controlada.
O detalhe de implementação varia.
A ideia principal não.
Outbox não é fila improvisada
Um erro comum é olhar para a tabela outbox e pensar:
"então agora meu banco virou uma fila".
Não exatamente.
A outbox não existe para substituir Kafka.
Ela existe para atravessar com segurança a fronteira entre a transação local e a publicação no broker.
O banco continua sendo o ponto de verdade da mudança local.
Kafka continua sendo o log distribuído que entrega eventos para outros consumidores.
A outbox é a ponte transacional entre os dois.
Ela guarda uma intenção confirmada:
"quando este pedido foi criado, este evento passou a precisar ser publicado".
Isso é bem diferente de tentar usar uma tabela como mecanismo geral de mensageria, com retenção infinita, múltiplos grupos de consumo, replay arbitrário, fan-out e semântica de streaming.
O papel dela é mais estreito.
E justamente por isso ela funciona bem.
Como o evento sai da outbox
Depois que o registro está na outbox, ele precisa chegar ao Kafka.
Existem duas abordagens comuns.
A primeira é polling.
Um worker consulta eventos pendentes, publica no Kafka e marca como publicado.
Algo como:
buscar eventos com published_at is null
publicar no Kafka
marcar published_at
É simples de entender e funciona para muitos sistemas.
Mas exige cuidado com concorrência, lock, paginação, retry, ordenação e volume.
A segunda é CDC, geralmente com ferramentas como Debezium.
Nesse modelo, a aplicação grava a outbox no banco e uma ferramenta de Change Data Capture observa o log transacional do banco, transforma as linhas da outbox em mensagens e publica no Kafka.
O ganho é que a publicação fica próxima do log real de alterações do banco.
O custo é operacional: mais infraestrutura, mais configuração, mais atenção com schema, roteamento, transforms e observabilidade.
Nenhuma das duas abordagens é universalmente melhor.
Polling costuma ser suficiente quando o volume é controlado e a equipe quer simplicidade.
CDC costuma fazer mais sentido quando o fluxo é crítico, o volume cresce ou a organização já opera esse tipo de infraestrutura com maturidade.
O importante é não confundir a ferramenta com o padrão.
Outbox é a decisão de persistir a intenção de publicação na mesma transação da mudança de negócio.
Polling ou CDC são formas de transportar essa intenção até o Kafka.
A duplicidade continua existindo
Outbox reduz perda de evento.
Não elimina duplicidade.
Esse ponto é essencial.
O publicador pode enviar o evento para o Kafka e cair antes de marcar a linha como publicada.
Quando ele voltar, pode publicar de novo.
Um conector CDC pode reenviar depois de uma falha.
Um producer pode retentar.
Um consumer pode receber a mesma mensagem mais de uma vez.
Isso não significa que o Outbox falhou.
Significa que o padrão resolveu uma parte do problema: não perder a intenção de publicação depois do commit do banco.
A outra parte continua exigindo idempotência.
Por isso, cada evento de outbox deveria carregar um identificador estável, como event_id, operation_id ou uma chave de negócio que permita deduplicação.
Consumers importantes precisam tratar repetição como comportamento esperado, não como bug raro.
Esse ponto conversa diretamente com o post sobre idempotência em consumers: se repetir a mensagem quebra o negócio, a arquitetura ainda não está pronta para Kafka em produção.
Ordem também precisa ser pensada
Outro detalhe que costuma aparecer tarde é ordenação.
Se a outbox publica eventos de um mesmo pedido, conta ou contrato, a chave usada no Kafka importa.
Dois eventos como:
OrderCreated;OrderCancelled.
precisam chegar aos consumidores em uma ordem coerente para aquele agregado.
Kafka só preserva ordem dentro da mesma partição.
Então a chave do evento deveria refletir a unidade onde a ordem importa.
Em muitos casos, isso significa usar orderId, customerId, accountId ou outro identificador do agregado.
Se o publicador joga eventos com chaves aleatórias, a outbox pode estar correta no banco e ainda assim produzir uma sequência difícil de consumir.
A tabela outbox garante que o evento foi registrado junto da transação.
Ela não escolhe automaticamente a chave certa do Kafka.
Essa escolha ainda é desenho de domínio.
Foi por isso que o post sobre key no Kafka insistiu tanto que chave não é detalhe técnico: ela define distribuição, ordem e escalabilidade.
O payload da outbox é contrato
Outra decisão importante é o que colocar no payload.
Uma outbox ruim vira apenas um depósito de JSON improvisado, sem versão, sem contrato e sem clareza de consumidor.
Uma outbox saudável trata o evento como contrato.
Isso significa pensar em:
event_type;- versão do schema;
- identificador do evento;
- identificador do agregado;
- timestamp de ocorrência;
- dados necessários para consumo;
- metadados de correlação e rastreabilidade.
O evento não deveria ser um dump cego da entidade do banco.
Banco é modelo interno.
Evento é contrato publicado.
Misturar os dois acopla consumidores ao desenho de persistência do produtor.
Esse é o mesmo princípio discutido em evento não é DTO: evento precisa representar um fato de negócio estável, não apenas a forma como uma classe ou tabela estava modelada naquele sprint.
O que observar em produção
Outbox sem observabilidade vira dívida silenciosa.
O sistema pode estar gravando eventos corretamente, mas falhando na publicação.
Nesse caso, o banco cresce, os consumidores atrasam e ninguém percebe até algum processo de negócio reclamar.
Algumas métricas são básicas:
- quantidade de eventos pendentes;
- idade do evento mais antigo ainda não publicado;
- taxa de publicação;
- taxa de erro por tipo de evento;
- número de retries;
- eventos travados por payload inválido;
- diferença entre criação na outbox e chegada no Kafka.
Também é importante ter uma política de retenção.
Depois que um evento foi publicado e não é mais necessário para auditoria operacional, ele pode ser arquivado ou removido conforme a necessidade do domínio.
Sem isso, a tabela cresce indefinidamente e começa a afetar o próprio banco transacional.
Outbox protege consistência.
Mas, se for abandonada sem manutenção, vira gargalo.
Quando usar Outbox Pattern
Outbox faz sentido quando uma mudança local precisa gerar um evento confiável para outros sistemas.
Casos típicos:
- pedido criado;
- pagamento aprovado;
- contrato assinado;
- saldo alterado;
- assinatura cancelada;
- estoque reservado;
- cadastro ativado.
Em todos esses casos, perder o evento depois do commit local cria uma diferença real entre o serviço dono do dado e o restante da arquitetura.
Quanto mais crítico o evento, mais forte fica o argumento para outbox.
Mas nem tudo precisa desse padrão.
Se o evento é apenas telemetria descartável, métrica auxiliar ou sinal que pode ser reconstruído facilmente, talvez uma publicação direta com retry seja suficiente.
O padrão tem custo.
Ele adiciona tabela, publicador, limpeza, monitoramento, decisões de schema e tratamento de duplicidade.
Usar outbox por reflexo em qualquer publicação também é sinal de arquitetura automática demais.
A pergunta certa é:
"se esse evento não for publicado depois do commit, o negócio fica inconsistente de um jeito relevante?"
Se a resposta for sim, outbox entra forte na conversa.
O que vale fixar
Outbox Pattern não faz mágica distribuída.
Ele não transforma banco e Kafka em uma transação única.
Ele não elimina duplicidade.
Ele não escolhe chave, schema, retenção ou estratégia de consumo por você.
O que ele faz é muito importante e mais específico:
ele garante que a mudança de negócio e a intenção de publicar o evento sejam persistidas juntas na mesma transação local.
Depois disso, a publicação pode falhar, repetir, atrasar e ser retomada com critério.
Sem outbox, uma queda entre banco e Kafka pode criar um estado invisível.
Com outbox, essa intenção fica registrada, durável e recuperável.
Em sistemas orientados a eventos, essa diferença separa um fluxo que "normalmente funciona" de um fluxo que consegue sobreviver às falhas reais de produção.
