O banco salvou os dados. O Kafka não recebeu a mensagem. E agora?
Em sistemas distribuídos, uma operação aparentemente simples pode esconder um problema sério de consistência.
O Dual-Write Problem aparece quando uma aplicação precisa atualizar dois recursos independentes — por exemplo, persistir um pedido no PostgreSQL e publicar um evento no Kafka — sem possuir uma transação atômica entre eles.
Imagine que uma aplicação precise executar duas operações:
- Persistir um dado no banco.
- Publicar um evento em um message broker.
Por exemplo:
Figura 1 — Dual write: a aplicação grava no banco de dados e publica no Kafka como duas operações independentes.
Na maioria das vezes, tudo funciona.
O problema começa quando alguma coisa falha exatamente entre essas duas operações.
Esse cenário é conhecido como Dual-Write Problem.
O problema
Imagine uma aplicação responsável por criar pedidos.
Quando um pedido é criado, precisamos:
- Salvar o pedido no PostgreSQL.
- Publicar um evento
OrderCreatedno Kafka.
Uma implementação comum poderia ser semelhante a:
public void createOrder(Order order) {
orderRepository.save(order); // Database (Processo 1)
kafkaTemplate.send( // Kafka (Processo 2)
"order-created",
new OrderCreatedEvent(order)
);
}
Visualmente:
Figura 2 — Fluxo de criação do pedido: persistência no PostgreSQL e publicação do evento OrderCreated no Kafka.
Parece simples.
Mas existe uma pergunta extremamente importante:
O que acontece se a aplicação falhar exatamente entre essas duas operações?
Por exemplo:
Figura 3 — Dual-Write Problem: o pedido é salvo no PostgreSQL, mas o evento OrderCreated não chega ao Kafka.
Agora temos:
PostgreSQL
ORDER 123
STATUS = CREATED
✅ Pedido existente
Enquanto no Kafka:
OrderCreated
❌ Evento inexistente
O banco sabe que o pedido existe.
Os serviços que dependem do evento não sabem.
Passamos a ter diferentes versões da realidade dentro do mesmo sistema.
Por que isso é perigoso?
Imagine que outros serviços dependam do evento OrderCreated.
Por exemplo:
Figura 4 — A ausência do evento no Kafka impede que serviços de pagamento, estoque e notificação reajam à criação do pedido.
Se o pedido foi salvo, mas o evento não foi publicado:
- o pagamento pode não ser iniciado;
- o estoque pode não ser reservado;
- o cliente pode não receber uma notificação;
- outros processos de negócio podem nunca começar.
O pedido existe no banco, mas parte do sistema simplesmente não sabe disso.
O problema deixa de ser apenas técnico.
Ele passa a afetar diretamente o negócio.
Não adianta dizer: "Basta usar uma transação"
Uma resposta comum para esse problema é utilizar uma transação.
Por exemplo, utilizando Spring:
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
kafkaTemplate.send(
"order-created",
new OrderCreatedEvent(order)
);
}
Agora temos uma transação.
Problema resolvido?
Não necessariamente.
Visualizando o problema: por que uma transação local não resolve automaticamente um ambiente distribuído?
O @Transactional, em uma configuração tradicional utilizando um banco relacional, controla principalmente a transação associada ao banco de dados.
Isso permite, por exemplo, realizar rollback das alterações referentes àquela transação.
Algo semelhante a:
BEGIN;
INSERT INTO orders (...);
COMMIT;
Porém, PostgreSQL e Kafka continuam sendo dois recursos independentes.
A aplicação está tentando coordenar duas operações que não fazem automaticamente parte da mesma transação atômica.
Podemos ter diferentes cenários.
Cenário 1 — Banco funciona e Kafka falha
Figura 5 — Cenário 1: o banco confirma a operação, mas a publicação no Kafka falha, gerando inconsistência.
Resultado:
Banco possui o pedido.
Kafka não possui o evento.
Temos uma inconsistência.
Cenário 2 — Kafka recebe, mas o banco falha
Dependendo da forma como o código está estruturado, podemos criar o cenário inverso.
Figura 6 — Cenário 2: o Kafka recebe o evento, mas a persistência no banco de dados falha.
Agora o Kafka possui um evento informando que um pedido foi criado.
Mas o pedido não existe no banco.
Novamente temos duas versões diferentes da realidade.
O verdadeiro problema
O problema principal é que essas operações não compartilham a mesma atomicidade.
Em uma operação realmente atômica esperamos:
0% ou 100%
Ou tudo acontece.
Ou nada acontece.
Seria algo como:
Figura 7 — Atomicidade ideal: banco de dados e broker concluiriam as duas operações como uma única unidade.
Ou:
Figura 8 — Em uma operação realmente atômica, uma falha faria ambas as operações serem revertidas.
Porém, como vimos anteriormente, o fato de apenas um dos dois processamentos ser concluído é justamente o problema.
Transactional Outbox Pattern
Uma das soluções mais conhecidas para esse problema é o Transactional Outbox Pattern.
A ideia principal é relativamente simples.
Em vez de tentar atualizar o banco e publicar diretamente no Kafka durante a mesma operação:
Figura 9 — Abordagem de dual write: a aplicação tenta atualizar o banco e publicar diretamente no Kafka durante a mesma requisição.
Passamos a fazer:
Figura 10 — Transactional Outbox Pattern: dados de negócio e evento são gravados na mesma transação local; a publicação no Kafka ocorre posteriormente.
Perceba que a aplicação grava no banco de dados, em uma única operação:
- os dados de negócio;
- o evento que precisa ser publicado.
Tudo dentro da mesma transação local do banco de dados.
A Outbox Table
Criamos uma tabela adicional no banco.
Por exemplo:
outbox_events
Ela pode possuir campos como:
id
aggregate_id
event_type
payload
created_at
status
Por exemplo:
id evt-123
aggregate_id order-999
event_type OrderCreated
status PENDING
created_at 2026-09-02
E o payload:
{
"orderId": 999,
"customerId": 42,
"total": 199.90
}
Agora temos uma única transação
A criação do pedido pode executar algo semelhante a:
BEGIN;
INSERT INTO orders (...);
INSERT INTO outbox_events (...);
COMMIT;
Visualmente:
Figura 11 — Uma única transação PostgreSQL persiste o pedido em orders e o evento em outbox_events.
Agora existe atomicidade entre essas duas escritas no banco.
Ou ambos são persistidos:
Figura 12 — Commit bem-sucedido: orders e outbox_events são persistidos atomicamente na mesma transação.
Ou ambos sofrem rollback:
Figura 13 — Rollback da transação: se a operação falhar, nem o pedido nem o evento da outbox são persistidos.
Isso elimina a dupla escrita entre o banco de dados e o broker durante a requisição principal.
Atomicidade
A atomicidade existe especificamente na execução da operação de persistência.
Isso acontece porque os dados de negócio e o registro da outbox estão dentro da mesma transação do banco de dados.
A publicação no Kafka continua acontecendo posteriormente.
Ou seja:
Figura 14 — Após o commit, um Message Relay lê os eventos da Outbox Table e os publica no Kafka.
Essa diferença é extremamente importante.
A aplicação não está tentando transformar PostgreSQL e Kafka em uma única operação atômica.
Ela está garantindo que:
Pedido criado
+
Evento pendente para publicação
sejam persistidos juntos.
Depois disso, outro componente é responsável pela publicação do evento.
Estratégias de acesso à tabela Outbox
Agora precisamos de algum componente responsável por retirar os eventos da tabela Outbox e publicá-los no broker.
Existem diferentes estratégias para fazer isso.
Por exemplo:
- polling publisher;
- Change Data Capture (CDC);
- ferramentas como Debezium;
- processos dedicados de Message Relay.
Mas esse será o tema do próximo artigo da série:
Padrões de Sistemas Distribuídos (#02) — Message Relay: consumindo uma tabela Outbox para manter a atomicidade
Nele, veremos como retirar os eventos da Outbox Table e publicá-los no Kafka de forma confiável.
Conclusão
O Dual-Write Problem aparece sempre que precisamos modificar dois recursos independentes e esperamos que ambos permaneçam consistentes.
No nosso exemplo:
PostgreSQL + Kafka
Uma transação local no banco não transforma automaticamente os dois recursos em uma única operação atômica.
O Transactional Outbox Pattern resolve esse problema mudando a estratégia.
Em vez de:
Banco
+
Kafka
durante a operação principal, fazemos:
orders
+
outbox_events
dentro da mesma transação PostgreSQL.
E posteriormente:
outbox_events
↓
Message Relay
↓
Kafka
Com isso, conseguimos garantir a atomicidade da escrita local e tornar a publicação do evento um processo confiável e recuperável.
Referências
AWS Prescriptive Guidance — Transactional Outbox Pattern
https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.htmlDebezium — Outbox Event Router
https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.htmlInspiração / estudo — Renato Augusto
https://youtu.be/voeWcHeYzLI
Próximo artigo
Padrões de Sistemas Distribuídos (#02) — Message Relay: consumindo uma tabela Outbox para manter a atomicidade
Se você trabalha com Java, Spring Boot, Kafka, arquitetura orientada a eventos ou sistemas distribuídos, acompanhe a série para os próximos padrões.














Top comments (0)