ADR-002 — Ruta caliente desde snapshot en memoria y auditoría asincrónica que nunca bloquea¶
Status: accepted (discovery Q5, 2026-09-03)
Contexto¶
El GatewayQR consulta a BeneficiosCenter en medio de un cobro y luego debe reenviar la intención al autorizador. El objetivo es responder en menos de 20 ms (p99). La configuración cambia pocas veces al día; las consultas llegan miles de veces al día. Cada consulta debe quedar auditada.
Alternativas¶
- Resolver desde un snapshot inmutable en memoria y auditar por una cola acotada, sin bloquear nunca la respuesta. Si la cola está llena, se responde igual, se cuenta la pérdida y se alerta.
- Resolver desde la base en cada consulta (con caché de segundo nivel). Latencia y disponibilidad atadas a SQL Server.
- Auditoría sincrónica: sin auditoría persistida no hay respuesta. Traza garantizada; una base lenta frena cobros.
Decisión¶
Alternativa 1.
- La configuración activa (beneficios, tipos de cliente, sucursales) se carga en un objeto
inmutable
ConfiguracionActiva. Cada request resuelve contra la referencia que tomó al entrar; nunca ve un estado intermedio. - Al guardar desde la consola se publica un evento interno que reconstruye el snapshot y lo intercambia atómicamente. Además hay un refresco periódico (por defecto cada 60 s) como red de seguridad.
- La auditoría se encola en una cola acotada (por defecto 10 000 elementos) y la escribe un consumidor en virtual threads, en lotes.
- Invariante: la respuesta al gateway jamás espera a la base de datos. Cola llena o base
caída implica responder igual, incrementar el contador
auditoria.perdidas, registrar un log de nivel ERROR con muestreo y exponer el contador para alerta.
Consecuencias¶
- ✅ Latencia de la ruta caliente independiente de SQL Server.
- ⚠️ Ventana de propagación de un cambio de configuración: inmediata en el proceso que guardó; acotada por el refresco periódico ante cualquier fallo del evento.
- ⚠️ En un incidente de base prolongado se pierde traza de auditoría. Se acepta porque el gateway conserva su propio registro de la Trx y porque un cobro frenado es peor que una traza faltante.
- Memoria: el snapshot es pequeño (cientos de beneficios como máximo). La cola acotada limita el consumo bajo backpressure.
Plan de implementación¶
resolucion/ConfiguracionActiva(record inmutable) yresolucion/ConfiguracionActivaHolder(AtomicReference).resolucion/BenefitResolver: función puraresolver(request, configuracion) -> resultado; sin Spring, sin I/O.auditoria/ColaAuditoriaconArrayBlockingQueue;offer()no bloqueante; consumidor enExecutors.newVirtualThreadPerTaskExecutor()con escritura en lotes.- Métricas Micrometer:
resolver.latencia,auditoria.encoladas,auditoria.perdidas,configuracion.version. - Evento
ConfiguracionCambiadapublicado por los servicios de administración; listener que reconstruye el snapshot. - Tarea programada de refresco con intervalo configurable.
Verificación¶
- Test: con la base detenida,
POST /resolverresponde 200 dentro del presupuesto yauditoria.perdidasincrementa (AC-18). - Test: dos requests concurrentes durante un intercambio de snapshot ven cada uno un estado consistente.
- Test: guardar un beneficio desde la API de administración cambia la respuesta de la siguiente consulta sin reinicio (AC-19).
- Prueba de carga: p99 < 20 ms con 200 req/s sostenidos durante 5 minutos (AC-39).