Saltar a contenido

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

  1. 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.
  2. Resolver desde la base en cada consulta (con caché de segundo nivel). Latencia y disponibilidad atadas a SQL Server.
  3. 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) y resolucion/ConfiguracionActivaHolder (AtomicReference).
  • resolucion/BenefitResolver: función pura resolver(request, configuracion) -> resultado; sin Spring, sin I/O.
  • auditoria/ColaAuditoria con ArrayBlockingQueue; offer() no bloqueante; consumidor en Executors.newVirtualThreadPerTaskExecutor() con escritura en lotes.
  • Métricas Micrometer: resolver.latencia, auditoria.encoladas, auditoria.perdidas, configuracion.version.
  • Evento ConfiguracionCambiada publicado 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 /resolver responde 200 dentro del presupuesto y auditoria.perdidas incrementa (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).