ADR-009: Historial de flujos como eventos append-only + proyección de estado¶
Estado: Propuesto Fecha: 2026-06-15 Autores: Giampiero (mantenedor principal)
Contexto¶
El módulo de flujos (RT-05) enruta documentos entre dependencias: asignaciones, reenvíos, devoluciones, visto bueno, cierres. El Orfeo legado mantenía un historial mutable (filas que se actualizaban), lo que hacía que el rastro de "quién tuvo qué y cuándo" se pudiera perder o sobreescribir — un problema de trazabilidad legal.
A la vez, la operación diaria necesita consultas de estado actual baratas: la bandeja de un usuario, los pasos abiertos, los vencimientos de SLA. Un historial puro de eventos no responde eso de forma directa sin recorrer todo el historial.
Hay que conciliar inmutabilidad del rastro con eficiencia del estado actual.
Decisión¶
flow_events (append-only) es la fuente de verdad del flujo; flow_steps (y la bandeja) es una proyección mutable reconstruible a partir de los eventos.
- Cada acción del flujo (asignar, reenviar, devolver, VoBo, cerrar) se registra como un evento inmutable en
flow_events. - El estado actual (
flow_steps, bandejas, pasos abiertos) se mantiene como proyección actualizada por un consumidor de esos eventos; puede reconstruirse desde cero reproduciendoflow_events. - Los mismos eventos se publican en Redis Streams (ver contrato de eventos) para que otros servicios reaccionen (notificaciones, búsqueda, SLA).
Implementación¶
-- Fuente de verdad: inmutable, append-only (en tenant_{slug})
CREATE TABLE IF NOT EXISTS flow_events (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
objeto_tipo TEXT NOT NULL, -- 'radicado' | 'expediente'
objeto_ref TEXT NOT NULL,
tipo_evento TEXT NOT NULL, -- 'asignado' | 'reenviado' | 'devuelto' | 'vobo' | 'cerrado'
from_dep TEXT,
to_dep TEXT,
actor TEXT NOT NULL,
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
payload JSONB NOT NULL DEFAULT '{}'::jsonb
);
-- Sin UPDATE/DELETE para el rol de aplicación (igual filosofía que ADR-008).
-- Proyección mutable: estado actual para bandeja/SLA (reconstruible)
CREATE TABLE IF NOT EXISTS flow_steps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
objeto_tipo TEXT NOT NULL,
objeto_ref TEXT NOT NULL,
dependencia TEXT NOT NULL,
estado TEXT NOT NULL, -- 'abierto' | 'cerrado'
asignado_en TIMESTAMPTZ NOT NULL,
vence_en TIMESTAMPTZ, -- SLA (días hábiles, vía E14)
last_event_id BIGINT NOT NULL -- hasta qué evento está proyectado
);
- El consumidor aplica cada
flow_eventaflow_stepsde forma idempotente (usalast_event_id). - El scheduler de SLA (ver catálogo de alertas) lee
flow_steps, no el historial completo.
Consecuencias¶
Positivas: - Rastro del flujo inmutable y reproducible — coherente con la filosofía de auditoría (ADR-008). - Estado actual eficiente para bandeja y SLA. - La proyección puede reconstruirse si se corrompe (los eventos son la verdad).
Negativas: - Doble escritura (evento + proyección) y complejidad del consumidor. - Consistencia eventual de la proyección respecto al evento (ventana corta; mitigable proyectando en la misma transacción para los casos críticos).
Alternativas consideradas¶
- Solo
flow_stepsmutable (modelo legado): descartado — es justamente la causa de la pérdida de historial que este ADR corrige. - Event sourcing completo (sin tablas de proyección, todo derivado en lectura): descartado por sobre-ingeniería; el híbrido evento+proyección da inmutabilidad sin penalizar la lectura.
Relacionados¶
- ADR-008 — misma filosofía append-only; los eventos de flujo también generan auditoría.
- Contrato de eventos (specDrive) —
flow_eventsse publica enorpycamcp.workflow.events.