ATLAS / INTEGRAÇÕES E EVENTOS / FABRÍCIO CASTRO
Gravar não é publicar.
Publicar não é confirmar.
Quando o negócio precisa gravar uma mudança e comunicar um evento, duas operações isoladas criam uma janela de falha. A Transactional Outbox grava a intenção de publicação junto do estado de domínio em uma transação local; um relay separado pode enviar depois. Reentregas são possíveis e precisam ser tratadas, nunca ocultadas por promessas de exactly-once.
01 / UMA MUDANÇA E DOIS SISTEMAS
O commit pode acontecer. O envio, não.
Considere uma solicitação de compras fictícia. O serviço registra o envio e depois tenta notificar uma integração externa. Se o processo morrer entre essas duas ações, o banco contém o novo estado, mas o destinatário pode nunca receber a informação. Inverter a ordem também falha: o destinatário pode receber um evento cujo estado de origem foi revertido.
Commit bem-sucedido, mensagem perdida.
Um comando SQL seguido de um POST HTTP deixa uma janela entre as duas ações. Retries ad hoc podem duplicar efeitos, e transação SQL não faz rollback de um serviço remoto.
Critério: o resultado deve ser recuperável sem fingir que o banco controla o destinatário.
A fila nasce junto da mudança.
O dado de negócio e o registro de evento são confirmados na mesma transação. Uma rotina posterior reivindica mensagens pendentes e registra seu progresso. Não há necessidade de um broker distribuído para compreender o padrão.
Limite: publicar uma mensagem e confirmar seu envio continuam ações separadas.
02 / TRANSAÇÃO LOCAL / SQL REAL
Negócio e intenção de evento confirmam juntos.
O laboratório representa uma transição draft → submitted. O UPDATE verifica tenant, identificador, estado e versão. O resultado alimenta o INSERT na Outbox dentro da mesma transação SQL. Se o evento não puder ser inserido, o banco também desfaz a alteração de negócio.
BEGIN;
-- Uma transição do negócio atualiza a solicitação e gera evento
-- na mesma transação SQL; rowCount=0 significa estado obsoleto.
WITH changed AS (
UPDATE purchase_requests
SET status = 'submitted', version = version + 1
WHERE tenant_id = $1 AND request_id = $2
AND version = $3 AND status = 'draft'
RETURNING tenant_id, request_id, version
)
INSERT INTO outbox_events
(tenant_id, event_id, request_id, aggregate_version,
event_type, payload)
SELECT tenant_id, $4::uuid, request_id, version,
'PurchaseSubmitted', $5::jsonb
FROM changed;
COMMIT;Exemplo didático resumido. O SQL físico usa identificador de schema aleatório validado e parâmetros do driver; não é uma migração do banco ERP.
Uma alteração, um registro para publicar.
O teste consulta a solicitação e a tabela de Outbox depois do commit. A chave única por organização, solicitação, versão e tipo impede registrar duas mensagens equivalentes para a mesma transição.
Erro na mensagem desfaz também o estado.
O ensaio injeta um payload incompatível com a constraint JSON do evento. A alteração de negócio deve permanecer em rascunho e nenhuma mensagem deve ficar disponível.
03 / VÁRIOS TRABALHADORES / FILA SQL
Um worker não deve disputar a mesma linha bloqueada.
O relay utiliza FOR UPDATE SKIP LOCKED para escolher uma mensagem elegível por tenant e reivindicá-la em uma instrução atômica. Isso reduz contenção entre consumidores concorrentes de uma fila. O PostgreSQL alerta que esse mecanismo apresenta uma visão inconsistente dos dados e não serve como critério geral de ordenação ou justiça entre mensagens.
WITH candidate AS (
SELECT tenant_id, event_id
FROM outbox_events
WHERE tenant_id = $4 AND attempts < 3
AND (
(status = 'pending' AND available_at <= clock_timestamp())
OR (status = 'leased' AND lease_until <= clock_timestamp())
)
ORDER BY created_at, tenant_id, event_id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE outbox_events AS o
SET status = 'leased', attempts = attempts + 1,
leased_by = $1, lease_token = $2::uuid,
lease_until = clock_timestamp() + ($3 * interval '1 second')
FROM candidate AS c
WHERE o.tenant_id = c.tenant_id AND o.event_id = c.event_id
RETURNING o.tenant_id, o.event_id, o.lease_token, o.payload;A query real retorna também tentativas e token do lease. O teste usa dois clientes: enquanto um mantém lock sobre uma mensagem, outro worker deve reivindicar uma mensagem diferente, sem duplicar a mesma reivindicação.
04 / RECONHECIMENTO E RECUPERAÇÃO
O token de posse impede um ACK atrasado de vencer.
Cada reivindicação gera um identificador de lease e prazo de validade. Após entrega confirmada pelo publisher, o worker tenta marcar a mensagem como sent usando tenant, ID, status, token atual e prazo ainda válido. Um worker antigo com lease expirado não pode apagar o progresso da nova tentativa.
- 01Claim
Reserva uma mensagem elegível com token novo, número de tentativa e prazo. A seleção é isolada por organização; não há autorização de worker implementada no laboratório.
- 02Publish
Envia o mesmo ID de evento como chave de idempotência ao destinatário. A operação externa não pode compartilhar transação PostgreSQL local sem protocolo distribuído próprio.
- 03ACK ou incerteza
Um ACK confirmado marca
sent. Se a conexão de banco falhar depois de publicar, o estado éack_unknown; o relay não deve inventar confirmação nem disparar retry imediato às cegas. - 04Lease expirado
A mensagem pode voltar a ser reivindicada. O teste impede ACK com token antigo, tenant incorreto ou confirmação repetida.
05 / BACKOFF E RECONCILIAÇÃO
Tentar novamente não é repetir para sempre.
O exemplo usa no máximo três tentativas e atrasos limitados. Falhas de publicação liberam a mensagem para novo processamento; ao atingir o limite, ela passa para dead, com código técnico não sensível. Um processo de reconciliação deve examinar a causa antes de autorizar eventual reprocessamento manual.
const result = await relayOnce(
verifiedTenantId,
workerId,
postgresOutboxAdapter,
idempotentPublisher,
);
// "sent" = ACK confirmado no banco local;
// "ack_unknown" = efeito externo pode ter acontecido;
// "retry_scheduled" = publicação falhou e nova tentativa foi agendada.
// Um broker real não foi criado neste capítulo.A função relayOnce implementa um único ciclo. Ela não inicia daemon, não configura credenciais e não envia mensagens reais; seus ports devem receber adaptadores confiáveis antes de qualquer aplicação operacional.
Uma mensagem esgotada não pode ficar travada.
Se o processo morrer com o terceiro lease, uma operação separada move a mensagem expirada para dead. Isso torna o problema visível para investigação, sem descartá-lo silenciosamente.
Há uma política explícita de tentativas.
Com teto de três tentativas, o laboratório não promete entrega eventual ilimitada. Recuperação exige monitoramento, responsável, autorização e registro de evidência.
06 / AT-LEAST-ONCE / CONSUMIDOR
O mesmo evento pode chegar duas vezes. O efeito não precisa acontecer duas vezes.
O caso adversarial aplica primeiro o efeito no consumidor, mas simula uma falha antes de registrar o ACK na Outbox. Depois que o lease expira, a mesma mensagem é reivindicada outra vez. A tabela de mensagens processadas usa chave composta por consumidor, tenant e evento; o efeito só é aplicado se o primeiro registro de processamento tiver sido inserido dentro da mesma transação do consumidor.
Gravar o identificador faz parte da decisão.
O teste usa INSERT … ON CONFLICT DO NOTHING RETURNING. Se já existe o evento para aquele consumidor, ele reconhece a duplicata e não repete o efeito. O registro de deduplicação e o efeito ficam na mesma transação local.
Uma tabela de teste não é um ERP real.
O efeito demonstrativo é gravado em outra tabela do mesmo PostgreSQL descartável. Isso comprova a lógica local quando executada, mas não simula HTTP externo, publicação Kafka, confirmação perdida na rede, semântica do ERP ou proteção exactly-once entre serviços.
SKIP LOCKED não garante FIFO global.
Uma mensagem bloqueada pode ser ultrapassada por outra. Quando a ordem dos eventos é requisito do domínio, serão necessários contratos de versão por agregado, particionamento, consumidor sequencial ou outra estratégia validada.
07 / TESTE REAL, BANCOS DESCARTÁVEIS
A prova precisa sair do PostgreSQL, não do comentário.
A suíte de integração cria um schema aleatório apenas após validar nome, host e confirmação do banco descartável. Ela executa testes em PostgreSQL real e remove o schema ao terminar. Os resultados devem ser registrados com SHA, versão do banco e logs, não inferidos pela presença dos arquivos.
# Com Docker local, Node e dependências já instaladas:
bash scripts/atlas/postgresql/outbox/run-local-docker.sh
# Com PostgreSQL local explicitamente descartável:
ATLAS_PG_TEST_URL="postgresql://atlas_lab:...@127.0.0.1:5432/fc_atlas_test_local" \
ATLAS_PG_TEST_CONFIRM=I_UNDERSTAND_DISPOSABLE_DATABASE \
pnpm --filter fanpage-frontend test:outbox:postgres
# Sem banco, apenas contratos e unidade TypeScript:
pnpm --filter fanpage-frontend test:outbox:contract
pnpm --filter fanpage-frontend test:outbox:unitO runner Docker publica uma porta aleatória somente em 127.0.0.1, usa credenciais temporárias e não consulta DATABASE_URL. Sem Docker/pg instalado, o teste real continua NOT_RUN.
MATRIZ DE TESTES / O QUE PRECISA PASSAR
Falhas que reproduzem problemas operacionais.
O laboratório está desenhado para verificar commit/rollback conjunto, dois workers com SKIP LOCKED, falha após o efeito e antes do ACK, lease token expirado, tentativas esgotadas e atomicidade do consumidor.
WRITEMutação e evento no mesmo COMMIT.
ROLLBACKEvento inválido desfaz a alteração de domínio.
CLAIMDois workers não reivindicam a mesma mensagem bloqueada.
REPLAYNova entrega não repete o efeito local já registrado.
ACKToken obsoleto não confirma uma mensagem.
RETRYFalhas repetidas levam a análise explícita em dead.
08 / FONTES E LIMITES
O padrão reduz uma janela de perda. Não elimina todas as falhas.
Uma solução de produção precisa tratar autorização do worker, secrets, schema migration, monitoramento de backlog, retries, dead-letter, privacidade, idempotência do consumidor externo, versões do evento e ordenação de mensagens por agregado quando exigida. O cenário permanece fictício e independente dos ambientes de ERP do usuário.
Chris Richardson — Transactional Outbox.
Descrição clássica do problema de dual-write, da gravação do evento na transação local, do relay e da possibilidade de duplicação após falhas.
Idempotent Consumer.
Explica a deduplicação com chave de mensagem processada e efeito de negócio dentro da mesma transação do consumidor.
PostgreSQL — SELECT locking.
Documenta SKIP LOCKED como alternativa útil para tabelas semelhantes a filas e esclarece que pode produzir uma visão inconsistente.
Concorrência e versionamento continuam válidos.
O estudo de PostgreSQL aprofunda versões esperadas, isolamento por tenant e transações concorrentes. A Outbox adiciona intenção durável de comunicação, sem substituí-los.
Código didático em branch: a presença de SQL, testes e exemplo de relay não comprova execução de banco, broker real, envio ao ERP, exactly-once, homologação visual, merge ou deploy em produção.