Voltar para a listaArquitetura • 9 min de leitura

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
Outbox pattern na prática com Spring Boot

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 é:

  1. Idempotência do consumidor — o relay vai republicar em caso de falha.
  2. Monitoramento do lag — alerta se outbox crescer mais rápido que consumir.
  3. Batch controlado — não trave o DB puxando 10k de uma vez.

Gotchas

  • Nunca dependa de ordem estrita do broker; use aggregate_id no 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