O Padrão Outbox sem Kafka
A maioria dos desenvolvedores acha que o Padrão Outbox exige Kafka. Não exige. Um polling publisher com PostgreSQL tem latência na casa dos segundos e zero infraestrutura extra — que é o trade-off correto para a grande maioria das sagas de negócio.
O Problema: Dual-Write
Todo sistema que escreve em um banco de dados e publica em um message broker enfrenta o mesmo problema fundamental: você não consegue fazer os dois atomicamente.
Considere a sequência:
- Escreva o pedido no PostgreSQL
- Publique
OrderCreatedno Kafka
Se sua aplicação cair entre os passos 1 e 2, o pedido existe no banco de dados mas o evento nunca foi publicado. Os serviços downstream nunca ficam sabendo. Seu sistema está silenciosamente inconsistente.
Se você inverter a ordem — publicar primeiro, depois escrever — você tem o problema espelhado: o evento foi publicado mas a escrita no banco falhou. Você anunciou algo que não existe.
Esse é o problema do dual-write. Não há como fazer dois sistemas independentes (um banco de dados e um message broker) participarem da mesma transação atômica sem infraestrutura especial.
O Padrão Outbox
A solução é converter o dual-write em um single-write. Em vez de escrever tanto no banco quanto no broker, você escreve apenas no banco — mas escreve em duas tabelas: sua tabela de domínio e uma tabela de outbox. As duas escritas acontecem na mesma transação ACID local.
BEGIN;
INSERT INTO orders (id, status, ...) VALUES (...);
INSERT INTO sagaweaw_outbox_messages (id, topic, payload, created_at)
VALUES (...);
COMMIT;
Um processo separado — o relay — faz polling na tabela de outbox, publica as mensagens no broker (se configurado) e as marca como entregues. O relay é a única coisa que toca o broker. A aplicação nunca o faz diretamente.
Isso transforma um problema impossível de atomicidade em dois sistemas em um problema tratável de durabilidade em um único sistema: se o relay falhar, o registro de outbox ainda está lá. O relay retoma de onde parou quando reinicia.
Por que as Pessoas Recorrem ao Kafka + Debezium
Quando os times descobrem o Padrão Outbox, frequentemente recorrem imediatamente ao CDC (Change Data Capture) — especificamente Kafka + Debezium, que captura mudanças no nível do WAL (Write-Ahead Log) do PostgreSQL.
O apelo é real:
- Latência sub-segundo: Os eventos do WAL são capturados à medida que acontecem, não em intervalos de polling
- Sem overhead de polling: O relay não precisa consultar o banco repetidamente
- Escalável para milhões de eventos por segundo: CDC é projetado para pipelines de alto throughput
Se você precisa dessas propriedades, CDC é a escolha certa.
Por que Isso é Exagero para a Maioria das Sagas
O ponto é: um step de saga normalmente leva entre 50ms e 500ms para ser executado. Uma chamada HTTP para um serviço de estoque. Uma escrita no banco. Uma cobrança via Stripe.
Se houver um lag de 2 segundos entre a escrita no outbox e o relay publicando a mensagem — porque o intervalo de polling é de 2 segundos — isso importa? Não. O step da saga que acionou a mensagem já levou 200ms. O próximo step nem vai começar até o atual terminar. Você já está operando na escala de segundos.
A matemática: duração_do_step (200ms) >> intervalo_de_polling (2000ms) é falso. Mas latência_total_da_saga (30s) vs intervalo_de_polling (2s) = 6,7% — desprezível.
Você não precisa de CDC sub-segundo para "reservar estoque → cobrar pagamento → enviar pedido."
O que o Sagaweaw Faz: Polling Publisher
O Sagaweaw usa um Polling Publisher — a implementação mais simples de relay que realmente funciona.
Um job @Scheduled faz polling em sagaweaw_outbox_messages a cada N segundos (configurável), publica as mensagens no Kafka se o Kafka estiver habilitado e as marca como entregues:
@Scheduled(fixedDelayString = "${sagaweaw.outbox.poll-interval:2000}")
public void pollAndRelay() {
List<OutboxMessage> pending = outboxRepository.findPending(batchSize);
for (OutboxMessage msg : pending) {
publisher.publish(msg);
outboxRepository.markDelivered(msg.getId());
}
}
Sem Debezium. Sem Kafka Connect. Sem cluster de conectores separado para operar, monitorar e atualizar. O polling publisher é algumas centenas de linhas de Java que vivem dentro da sua aplicação.
Kafka é Opt-In
Essa é a parte que surpreende a maioria dos desenvolvedores: Kafka é opcional no Sagaweaw.
sagaweaw:
kafka:
enabled: false # o padrão
Com kafka.enabled=false, a tabela de outbox ainda é escrita. O polling publisher ainda roda. As mensagens são marcadas como entregues imediatamente (efetivamente um no-op para o broker). Você obtém a durabilidade do padrão outbox — o registro do que aconteceu — sem precisar de um cluster Kafka rodando no seu ambiente de desenvolvimento ou staging.
Quando você estiver pronto para adicionar Kafka — porque você realmente precisa de fan-out assíncrono, ou está integrando com outros sistemas — você muda a flag e configura o broker. Os registros de outbox fluem sem alterações.
Isso significa que você pode adotar o padrão outbox no dia um, validar a lógica das suas sagas, ir para produção e adicionar Kafka depois, quando tiver uma razão concreta para precisar dele. Não porque um post de blog disse que sistemas distribuídos precisam de Kafka.
Quando CDC Vale a Pena
Para ser direto: há casos em que você quer CDC e Debezium.
- Você está processando mais de 10.000 mensagens por segundo
- Você tem um SLA de latência sub-segundo entre uma escrita e seu efeito downstream
- Você já roda Kafka em produção e tem expertise operacional para rodar o Kafka Connect
- Você precisa fazer fan-out para muitos consumidores independentes sem o relay se tornar um gargalo
Nenhum desses casos tipicamente se aplica a um serviço Spring Boot fazendo orquestração de sagas para um sistema de gestão de pedidos ou pagamentos. Eles se aplicam a plataformas de event streaming, analytics em tempo real e pipelines de dados de alto throughput.
Conheça seu SLA antes de escolher a infraestrutura. Se a latência da sua saga já é medida em segundos, polling com intervalos de 2 segundos é invisível.
A Restrição Honesta
Polling tem latência. Se você precisa de entrega garantida sub-segundo entre serviços — não apenas dentro de uma saga, mas como SLA para consumidores downstream — você quer CDC.
Para orquestração de sagas, essa restrição raramente se aplica. O coordenador da saga é quem sequencia os steps. Consumidores downstream não conduzem o caminho de execução da saga. O outbox é um registro e um mecanismo de entrega, não um stream de eventos de baixa latência.
Comece com o polling publisher. Adicione CDC quando você conseguir medir o problema de latência em produção, não antes.
Join the discussion!
Architecture is about trade-offs. What do you think about the decisions made in "O Padrão Outbox sem Kafka"? Share your scenarios, ask questions, and debate with other engineers in the Sagaweaw community.
Comment on GitHub Discussions