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.
Junte-se ao debate!
Arquitetura é feita de trade-offs. O que você achou das decisões tomadas em "O Padrão Outbox sem Kafka"? Compartilhe seus cenários, tire dúvidas e debata com outros engenheiros da comunidade Sagaweaw.
Comentar no GitHub Discussions