Outbox-pattern
Outbox es un patrón arquitectónico en el que un servicio de dominio registra un cambio de negocio y el evento correspondiente en una sola transacción local en su almacenamiento. La publicación de un evento en un bus/cola externo se realiza de forma asíncrona por un proceso seguro independiente (publisher) que lee la tabla 'outbox' y retransmite las entradas. Este enfoque elimina la carrera «primero en el DB, luego en el bus» y asegura una entrega confiable incluso en caso de fallas.
1) Cuándo aplicar
Adecuado:- Microservicios y monolitos modulares con eventos entre contextos.
- Debe asegurarse de que «el estado registrado ↔ el evento no puede perderse».
- Se necesita idempotencia y una retransmisión controlada.
- Son críticas las transacciones globales rígidas en múltiples recursos (mejor TSS/sagas con contratos explícitos).
- No hay una fuente de verdad dedicada (el estado no se almacena donde se genera el evento).
2) Objetivos y propiedades
Escritura atómica: registro de dominio + outbox - en una sola transacción.
Publicación At-least-once: permitimos la repetición, excluimos la pérdida.
Idempotencia del consumidor: protección contra tomas en el lado de los suscriptores.
Eficacia exactly-once: se logra mediante una combinación de outbox + idempotent consumer + dedup.
Telemetría clara: correlación entre operaciones comerciales y eventos.
3) Esquema de datos (ejemplo)
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) Plantilla transaccional (application layer)
pseudo begin tx domainChange () # INSERT/UPDATE in domain table insert into outbox (event) # event with aggregate/tenant commit tx keys
Si el commit tiene éxito, el evento de outbox está garantizado. Si la aplicación cae después del commit, el pablicher se pondrá al día.
5) Publisher (reader → publisher)
Tareas:- Leer periódicamente eventos no publicados ('published _ at IS NULL' y 'available _ at <= now ()'), batches.
- Intentar publicar en bus/cola; si tiene éxito, marque 'published _ at'.
- Si el error es aumentar 'attempts', poner' available _ at 'para el futuro (backoff exponencial), escribir' error '.
- Respetar los límites de tenantes/claves (fairness), no bloquear el producto.
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) Idempotencia y deduplicación
En el lado del 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: cuando se recibe un evento, primero se intenta 'INSERT' en 'inbox'; si el conflicto de clave es un evento ya procesado → «no-op». A continuación, la lógica empresarial.
En el lado del pablisher: 'Idempotency-Key' en los titulares (por ejemplo, 'event _ id') para que el bus/broker/proxy pueda filtrar duplicados.
7) Orden y causalidad
El orden local por 'aggregate _ id' se proporciona ordenando 'occurred _ at' y publicando 'por clave'.
Para los buses de registro con lotes - lote con la clave 'aggregate _ id '/' tenant _ id' para que los eventos de un solo agregado estén en el mismo lote.
Si el orden es crítico, evite las carreras de pablisher interpotal con una sola llave.
8) CDC (Change Data Capture)
Puede utilizar CDC en lugar del pablista activo: el motor lee el registro de transacciones de la DB y transmite las líneas 'outbox' al bus. Pros - carga mínima en el DB, secuencia exacta, sin polling. Contras - complicación de la operación y atadura a la especificidad del DBM. Ambos enfoques son válidos; elija por competencias y SLO.
9) Errores, DLQ y Redrave
Retryable (red, límites) - aumentar 'attempts', posponer' available _ at '(exponential backoff + jitter).
Non-retryable (esquema/contrato no válido) - Transferible a DLQ/Dead-Letter Topic con ricos metadatos.
Redrave seguro: batch, rate-limit, validación del esquema, prioridad por debajo del tráfico prod.
10) Multi-tenencia y límites
Etiquetas obligatorias: 'tenant _ id', 'plan', 'región' - en 'outbox. headers`.
Fairness Per-tenant: pablisher distribuye «ventanas» de publicaciones y límites de intento a los inquilinos.
Residency: Almacenar outbox en la misma región donde los datos de dominio; publicación interregional: sólo agregados/resúmenes.
11) Seguridad y cumplimiento
Edición PII en payload/headers sobre las políticas de tenant/región.
Firma/cifrado de carga útil si el bus es «ajeno».
Auditar todas las transiciones de estado: creado, publicado, error, redrive.
12) Observabilidad
Métricas:- Lag de publicación ('now - occurred_at' p50/p95/p99).
- Porcentaje de aciertos, porcentaje de errores, distribución de causas.
- Tamaño del outbox (cole-in no publicado), intentos/sec.
- Gráficos per-tenant throughput y lag.
- Correlación 'event _ id '/' aggregate _ id '/' saga _ id'; durmiendo «db-tx», «publish», «retry».
- Anotaciones: 'attempt',' backoff _ ms', 'dlq = true'.
- Breves grabaciones para el éxito; detalles completos por error/redrave.
13) Pruebas y caos
Atomicity test: artificialmente «caemos» después de commit una transacción de dominio antes de la publicación - el evento está obligado a salir más tarde.
Prueba duplicada: publicamos el mismo evento varias veces - el consumidor realiza exactamente un efecto (inbox).
Prueba de orden: paquete de eventos de un solo agregado - verificación de secuencia/idempotencia.
Chaos: fracaso del bróker, aumento de la latencia de la DB, split-brain pablishers, clock-skew.
14) Plantillas de configuración (ejemplo)
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) Integración con sagas y retraídas
Outbox - «transporte de seguridad» para los pasos de la saga: una transacción local escribe un efecto y un comando/evento; publicación - confiable y dosificable.
Las políticas de repetición y backoff deben estar alineadas con 'Retry-After' y Circuit Breaker; evite la «tormenta de retraídas».
16) Errores típicos
Escribir un evento después de un commit de estado de dominio: es posible que se pierda al caer.
No hay índices/archivos en 'outbox' → el aumento del retraso en la publicación.
Pablischer sin 'SKIP LOCKED' o sin charding - competencia y bloqueo.
Falta de idempotencia en los consumidores - tomas y efectos secundarios.
Mezcla PII sin enmascarar en DLQ/logs.
Una sola cola de publicación global sin fairness: un tenant «ruidoso» inhibe a todos.
La falta de monitoreo de la laguna → las degradaciones latentes.
17) Selección rápida de la estrategia
Nivel de inicio: polling desde el DB, batch 100-500, full-jitter backoff, inbox en los consumers.
Alta carga: CDC del registro de transacciones, charding por 'tenant _ id/aggregate _ id', WFQ por inquilinos.
Orden estricto por agregación: publicación en serie por clave (mutex), partición de topic por clave.
Cumplimiento/PII: cifrado de payload, revisión en DLQ, outbox regionales.
18) Lista de verificación antes de la venta
- Los cambios de dominio y la entrada en 'outbox' ocurren en una sola transacción.
- Pablischer maneja batches, usa 'SKIP LOCKED', backoff con jitter y límites.
- Los consumistas son idempotentes (tabla 'inbox '/dedup magazine).
- El DLQ y el redrive seguro están configurados.
- Métricas de errores/errores y alertas en los umbrales p95/p99.
- El orden de la clave está garantizado (lotes/serialidad).
- Archivo/retiro de 'outbox' y limpieza de registros publicados.
- Políticas PII y auditoría de saltos de estado.
- Pruebas de caída entre commit y publicación, duplicados y orden.
- Documentación de contratos de eventos (diagramas/versiones/compatibilidad).
la Conclusión
El patrón outbox transforma el conjunto «frágil» de «bus ↔ bus» en un transportador fiable: fijación de estado atómico, publicación garantizada (aunque sea «al menos una vez»), suscriptores idempotentes y redrive controlado. Con la telemetría correcta, los límites y la disciplina de los circuitos, da un comportamiento práctico exacto-once, reduciendo la complejidad de las transacciones distribuidas y aumentando la resistencia del sistema a fallas y cargas máximas.