Outbox pattern na prática com Spring Boot
Como garantir entregas at-least-once sem depender de transações distribuídas.
Autor: Danilo FernandoPublicado em 01 de março de 2025
O problema
Publicar eventos de forma confiável junto de uma transação de banco é uma das armadilhas clássicas de sistemas distribuídos. Se você grava no Postgres e depois publica no Kafka:
@Transactional
public void confirm(Order order) {
orderRepository.save(order); // ✅ transação do banco
kafkaTemplate.send("orders", order); // ❌ fora da transação
}
Basta um crash entre as duas chamadas para o banco dizer “confirmado” e o mundo nunca saber.
Outbox pattern
A ideia é gravar o evento na mesma transação do banco, em uma tabela
outbox. Um relay lê a tabela e publica de fato.
Schema mínimo
CREATE TABLE outbox (
id UUID PRIMARY KEY,
aggregate VARCHAR(64) NOT NULL,
event_type VARCHAR(128) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
CREATE INDEX outbox_unpublished_idx
ON outbox (created_at) WHERE published_at IS NULL;
Relay
Um job simples lê eventos não publicados, envia ao broker e marca como publicado. O importante é:
- Idempotência do consumidor — o relay vai republicar em caso de falha.
- Monitoramento do lag — alerta se
outboxcrescer mais rápido que consumir. - Batch controlado — não trave o DB puxando 10k de uma vez.
Gotchas
- Nunca dependa de ordem estrita do broker; use
aggregate_idno particionamento. - Não faça dead-letter dentro da mesma tabela — crie
outbox_failed. - Sempre grave o schema version do payload.
Quando evitar
Se o seu sistema não precisa de garantia at-least-once, o outbox é overkill.
Um @TransactionalEventListener do Spring resolve 80% dos casos.
#eventos #spring-boot #kafka