ransactional Outbox Publisher
8.1 Problema a resolver
Sin Transactional Outbox, es posible inconsistencia:
Solicitud guardada en PostgreSQL ✓
Fallo al publicar en Pub/Sub ✗
→ Resultado: solicitud huérfana, nunca se procesa en ERP.
8.2 Diseño del patrón
8.3 Tabla outbox_event
| Columna | Tipo | Nota |
|---|---|---|
id (PK) | BIGINT/BIGSERIAL o UUID | Secuencial conviene para polling ordenado; UUID si es global |
aggregate_type | VARCHAR | PURCHASE_ORDER_REQUEST |
aggregate_id | UUID | requestId |
event_type | VARCHAR | PURCHASE_ORDER_REQUESTED |
payload | JSONB | Envelope canónico completo |
partnerId, companyId, sourceSystem | VARCHAR | Indexado para filtros y métricas |
status | ENUM/SMALLINT | 0=PENDING, 1=PUBLISHED, 2=PUBLISH_FAILED_RETRY, 3=IGNORED |
created_at | TIMESTAMPTZ | |
processed_at | TIMESTAMPTZ NULL | |
retry_count | SMALLINT DEFAULT 0 | |
last_error | TEXT NULL |
- Índices:
(status, created_at)para pick;(aggregate_id, event_type)unique TBD. - Unique: opcionalmente
(aggregate_type, aggregate_id, event_type, retry_group)— pero para MVP basta con queprocessed_eventcontrole duplicados.
8.4 Tabla processed_event (idempotent consumer + publisher)
| Columna | Tipo | |
|---|---|---|
event_id UUID (PK) | eventId del envelope | |
processed_at TIMESTAMPTZ | ||
consumer VARCHAR | outbox-publisher o fusion-worker | |
request_id UUID | Index |
8.5 Selección de eventos pendientes
BEGIN;
SELECT * FROM outbox_event
WHERE status = 0
ORDER BY created_at ASC
LIMIT 20
FOR UPDATE SKIP LOCKED;
-- para cada fila: publish Pub/Sub
UPDATE outbox_event
SET status = 1,
processed_at = now(),
retry_count = retry_count + 1
WHERE id = ?;
INSERT INTO processed_event (event_id, processed_at, consumer, request_id)
VALUES (?, now(), 'outbox-publisher', ?)
ON CONFLICT DO NOTHING;
COMMIT;
8.6 Retries y manejo de fallos de publicación
| Caso | Política |
|---|---|
| Pub/Sub timeout/unavailable | Resilience4j Retry con exponential backoff. |
| Max attempts en memoria TBD 3-5 | Marcar status=PUBLISH_FAILED_RETRY; alerta; proceso normal en polling. |
| Lag persistente > TBD 5 minutos | Alert on-call. |
| Evento inválido (JSON corrupto) | Marcar IGNORED + audit severidad ALTA. |
8.7 Consideraciones de despliegue
- Polling cadencia: cada 100-500ms (configurable). Alternativa: PostgreSQL LISTEN/NOTIFY para reducir latencia (sobre campo status). MVP: polling simple.
- Concurrencia: múltiples réplicas;
SKIP LOCKEDgarantiza que cada evento se bloquee por una sola instancia. - Complemento: métrica
nexus_outbox_lag_seconds=now() - min(created_at)para eventosPENDING. - CDC (opcional fases posteriores): Debezium connector → Pub/Sub, pero incrementa complejidad; no MVP.