GH GambleHub

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.
No es adecuado:
  • 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.
Pseudocódigo:
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
💡 'FOR UPDATE SKIP LOCKED' elimina la competencia de los tablistas.

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.
Trayendo:
  • Correlación 'event _ id '/' aggregate _ id '/' saga _ id'; durmiendo «db-tx», «publish», «retry».
  • Anotaciones: 'attempt',' backoff _ ms', 'dlq = true'.
Registros:
  • 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.

Contact

Póngase en contacto

Escríbanos ante cualquier duda o necesidad de soporte.¡Siempre estamos listos para ayudarle!

Telegram
@Gamble_GC
Iniciar integración

El Email es obligatorio. Telegram o WhatsApp — opcionales.

Su nombre opcional
Email opcional
Asunto opcional
Mensaje opcional
Telegram opcional
@
Si indica Telegram, también le responderemos allí además del Email.
WhatsApp opcional
Formato: +código de país y número (por ejemplo, +34XXXXXXXXX).

Al hacer clic en el botón, usted acepta el tratamiento de sus datos.