Pattern Outbox
Outbox é um pattern arquitetônico em que o serviço de domínio grava uma mudança de negócio e um evento apropriado em uma transação local para o seu armazém. A publicação do evento em um pneu/fila externo é executada de forma assinhrônica por um processo de segurança individual (publisher) que lê a tabela 'outbox' e retransmite as gravações. Esta abordagem elimina a corrida «primeiro no banco de dados, depois no pneu» e permite uma entrega segura, mesmo em casos de falhas.
1) Quando aplicar
Adequado:- Microsséries e monólitos modulares com eventos entre contextos.
- Assegure-se de que o «estado registado ↔ não pode perder-se».
- Precisamos de idempotidade e reaproveitamento controlado.
- As transações globais rígidas em vários recursos (melhor que o TCS/saga com contratos explícitos) são cruciais.
- Não há origem de verdade selecionada (o estado não está armazenado onde o evento é gerado).
2) Objetivos e propriedades
Atomic write: gravação de domínio + outbox - em uma transação.
At-least-once publicação: permitindo a repetição, excluindo a perda.
Idempotação dos consumidores: protecção contra suplentes ao lado dos seguidores.
Exactly-once eficiente: obtido com a combinação outbox + idempotent consumer + dedup.
Telemetria clara, correlação de negócios e eventos.
3) Esquema de dados (exemplo)
sql
-- Domain table (example: orders)
CREATE TABLE orders (
id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
status TEXT NOT NULL,
total_amount NUMERIC(12,2) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT now()
);
-- Outbox
CREATE TABLE outbox (
id UUID PRIMARY KEY, -- event_id aggregate_type TEXT NOT NULL, -- 'order'
aggregate_id UUID NOT NULL, -- order_id tenant_id TEXT NOT NULL,
type TEXT NOT NULL, -- 'OrderCreated'
payload JSONB NOT NULL, -- serialized headers event JSONB NOT NULL DEFAULT '{}':: jsonb,
occurred_at TIMESTAMP NOT NULL, -- time in domain transaction available_at TIMESTAMP NOT NULL, -- earliest publish time (backoff)
published_at TIMESTAMP, - is filled by the attempts INT NOT NULL DEFAULT 0,
error TEXT
);
CREATE INDEX ON outbox (available_at) WHERE published_at IS NULL;
CREATE INDEX ON outbox (tenant_id, available_at) WHERE published_at IS NULL;
4) Modelo de transação (aplicação layer)
pseudo begin tx domainChange () # INSERT/UPDATE in domain table insert into outbox (event) # event with aggregate/tenant commit tx keys
Se a empresa tiver sucesso, o evento no outbox é garantido. Se a aplicação cair depois de uma comitiva, o Pablicher consegue.
5) Pablicher (reader → publisher)
Tarefas:- Ler eventos não publicados periodicamente ('published _ at IS NULL' e 'available _ at <= now ()') com batches.
- Tentar publicar no pneu/fila; ao ter êxito, comemorar «published _ at».
- Erro: aumentar 'attempts', colocar 'available _ at' para o futuro (exponential backoff), escrever 'erro'.
- Respeitar limites de tenantes/chaves (fairness), não bloquear .
pseudo loop:
events = select from outbox where published_at is null and available_at <= now()
order by occurred_at limit BATCH_SIZE for update skip locked
for e in events:
try:
broker. publish(topicFor(e), serialize(e. payload), headers(e))
markPublished(e. id, now())
except Retryable:
backoff = computeBackoff(e. attempts)
reschedule(e. id, now()+backoff, attempts+1, last_error)
except NonRetryable:
moveToDLQ (e) or markError (e) # by sleep (POLL_INTERVAL) policy
6) Idempotidade e dedução
Do lado do consumidor (Inbox/Idempotency store):sql
CREATE TABLE inbox (
consumer_name TEXT,
event_id UUID,
processed_at TIMESTAMP NOT NULL,
PRIMARY KEY (consumer_name, event_id)
);
Algoritmo: ao receber um evento, primeiro «INSERT» em «inbox»; se o conflito de chave já foi processado → «no-op». A seguir, a lógica dos negócios.
«Idempotency-Key» no headers (por exemplo, «event _ id») para que o pneu/corretor/proxy possa filtrar as duplicações.
7) Ordem e causalidade
A ordem local por 'aggregate _ id' é garantida pela triagem de 'occurred _ at' e pela publicação de 'chave'.
Para os pneus logs com particionização - particione com a chave 'aggregate _ id '/' tenant _ id' para garantir que os eventos de uma unidade estejam na mesma partishene.
Se a ordem é crítica, evite as corridas interflexivas do pablicher com uma chave.
8) CDC (Change Data Capture)
Você pode usar o CDC em vez do pablicher ativo: o motor lê o registro de transações da BD e transmite as linhas 'outbox' para o pneu. Os benefícios são a carga mínima de BD, a sequência exata, a falta de meia linha. Contras - Complicação da operação e alinhamento à especificidade do DDC. Ambas as abordagens são validadas; selecione por competência e SLO.
9) Erros, DLQ e redrave
Retryable (rede, limites) - Aumentamos 'attempts', adiamos 'available _ at' (exponential backoff + jitter).
Não-retryable (esquema/contrato) - transferido para DLQ/Dead-Letter Topic com metadados ricos.
Redrave seguro: batch, rate-limit, validação do circuito, prioridade abaixo do tráfego prod.
10) Multi-tenência e limites
Marcas de formatação obrigatórias: «tenant _ id», «place», «region» em «outbox». headers`.
Per-tenant fairness: O pablicher distribui «janelas» de publicações e limites de tentativas por locatários.
Residency: armazene o outbox na mesma região onde os dados de domínio; Publicação interregional - apenas agregados/resumos.
11) Segurança e conformidade
Edição PII em payload/headers sobre a política do tenante/região.
Assinar/encriptar carga útil se o pneu for «alheio».
Auditoria de todas as transições de estado: criado, publicado, erro, redrave.
12) Observabilidade
Métricas:- A lista de publicação ('now - accurred _ at' p50/p95/p99).
- Taxa de sucesso, proporção de erros, distribuição de causas.
- Tamanho do outbox (em não-publicado), tentativas/segundos.
- Gráficos para-tenentes throughput e lag.
- Correlação 'event _ id '/' agregate _ id '/' saga _ id'; span «db-tx», «publish», «retry».
- Anotações: 'attempt',' backoff _ ms ',' dlq = true '.
- Breves registros de sucesso; detalhes completos para erro/redrave.
13) Testes e caos
Atomicity teste: «caímos» artificialmente após uma transação de domínio da empresa antes da publicação - o evento é obrigado a sair mais tarde.
Duplicate teste: Publicamos o mesmo evento várias vezes - a consoante executa exatamente um efeito (inbox).
Teste Order: conjunto de eventos de um único dispositivo - verificação de sequência/idempotação.
Chaos: corretora falha, aumento da latência de BD, split-brain pablishers, clock-skew.
14) Modelos de configuração (exemplo)
yaml outbox:
poll_interval_ms: 200 batch_size: 200 order_by: occurred_at backoff:
strategy: exponential_full_jitter initial_ms: 250 max_ms: 10_000 max_attempts: 20 fairness:
per_tenant_parallelism: 4 per_key_serial: true
publisher:
rate_limit_per_sec: 500 headers:
idempotency_key: event_id schema_version: v3 dlq:
enabled: true topic: myapp. events. dlq include_metadata:
- error
- attempts
- source_table
- tenant_id
- aggregate_id
15) Integração com sagas e retraias
Outbox - «transporte de segurança» para os passos da saga: transação local escreve o efeito e comando/evento; a publicação é confiável e dosável.
Políticas de repetição e backoff devem ser alinhadas com 'Retry-After' e Circuito Breaker; Evite «tempestade de retrações».
16) Erros típicos
Escrevendo um evento após uma comitiva de domínio - possível perda de queda.
Sem índice/arquivo em 'outbox' → aumento do atraso na publicação.
Pablicher sem 'SKIP LOCKED' ou sem 'charding' - concorrência e bloqueio.
A falta de idempotidade dos consumidores - duplos e efeitos colaterais.
Mistura de PII sem disfarce em DLQ/logs.
Uma única fila global de publicação sem fairness - um tenente «barulhento» trava todos.
Falta de monitoramento da laje → degradação oculta.
17) Escolha rápida de estratégia
Nível inicial: polling de BD, batch de 100-500, full-jitter backoff, inbox em consoadores.
Alta carga: CDC do diário de transações, charding por 'tenant _ id/agregate _ id', WFQ por locatário.
Ordem rígida por unidade: publicação em série per key (mutex), particionização de top com chave.
Complaens/PII: criptografia payload, redação em DLQ, outbox regional.
18) Folha de cheque antes de vender
- As alterações de domínio e a gravação em 'outbox' ocorrem em uma transação.
- O pablicher processa batches, usa 'SKIP LOCKED', backoff com jitter e limites.
- Os conceituadores são idimpotentes (tabela 'inbox '/registro de dedução).
- DLQ configurado e redrave seguro.
- Métricas de laje/erro e alertas nas liminares p95/p99.
- A ordem da chave está garantida (partituras/série).
- Arquivo/retenchn 'outbox' e limpeza de gravações publicadas.
- Políticas PII e auditoria de transições de estados.
- Testes de queda entre commitas e publicações, duplicados e ordem.
- Documentação dos contratos do evento (esquemas/versões/compatibilidade).
Conclusão
O Outbox-Pattern transforma o ligamento «frágil» do «pneu ↔ BD» em uma linha de montagem confiável: fixação atômica do estado, publicação garantida (mesmo que «pelo menos uma vez»), assinantes idimpotentes e redrave controlada. Com a telemetria correta, os limites e a disciplina dos circuitos, ele oferece um comportamento exactly-once prático, reduzindo a complexidade das transações distribuídas e aumentando a resistência do sistema a falhas e cargas de pico.