Ir al contenido

Casos de estudio

Problemas reales y las decisiones que tomé

Cada caso sigue la misma estructura: contexto, problema, mi rol, la solución, las decisiones con su contrapartida y qué haría distinto hoy. Los sistemas están descritos de forma genérica por confidencialidad.

Seguridad e identidad

Identidad multiempresa sobre un proveedor de identidad extendido

Cómo dar acceso a usuarios de muchas empresas distintas, con identidades que no son únicas globalmente y una base de usuarios heredada que no podía cambiar de contraseña.

Contexto

Plataforma de facturación electrónica usada por muchas empresas distintas. Cada empresa tiene sus propios usuarios, sus propios productos contratados y su propia identidad visual. Había además una base de usuarios heredada de la plataforma anterior.

Problema

  • El identificador de un usuario no es único globalmente: el mismo nombre de usuario puede existir en dos empresas distintas, así que el login necesita el identificador fiscal de la empresa como parte de la identidad.
  • Los usuarios heredados tenían contraseñas guardadas con hashes antiguos, con capas adicionales de codificación. Pedir a todos un reset era inaceptable para el negocio.
  • Cada empresa esperaba ver su logo y su aviso de privacidad en la pantalla de acceso.

Mi rol

Autor de las dos librerías de extensión y del repositorio de temas; único contribuyente en tres de las cuatro piezas.

Solución

  • Un Authenticator SPI compone la identidad interna a partir de empresa + usuario y valida el identificador fiscal (formato y pertenencia).
  • Si el usuario todavía no existe en el proveedor de identidad, un verificador propio comprueba la credencial heredada con comparación en tiempo constante y, si es válida, lo aprovisiona en ese mismo login con sus roles y grupos derivados de los productos contratados.
  • La migración del resto de la empresa se lanza en hilos virtuales fuera del hilo de petición, para no penalizar el tiempo de login.
  • Un FormAction SPI añade el preregistro de usuarios secundarios con validación de empresa y de duplicados.
  • Un EmailSenderProvider SPI hace que los correos del proveedor de identidad salgan por el servicio de correo de la plataforma: una sola plantilla, un solo remitente, un solo log.
  • Dos temas de login en FreeMarker resuelven el logo por dominio, con enrolamiento TOTP por QR y modo manual, selección entre varios dispositivos OTP, 9 idiomas y atributos de accesibilidad.

Decisiones y contrapartidas

Extender el proveedor de identidad en vez de construir autenticación propia
Se hereda gestión de sesiones, tokens, MFA y consentimiento. El coste es quedar atado al ciclo de vida de sus SPI: cada actualización mayor obliga a revisar interfaces.
Migración perezosa al primer login, no masiva
Nadie pierde el acceso y no hay ventana de mantenimiento; a cambio hay que mantener el camino heredado durante toda la transición y decidir cuándo apagarlo.
Comparación en tiempo constante aunque el hash sea antiguo
El algoritmo heredado es débil por definición, pero eso no justifica filtrar información por el tiempo de respuesta.
Delegar el correo del proveedor de identidad a un servicio propio
Unifica plantillas y trazabilidad, pero convierte al servicio de correo en dependencia del flujo de recuperación de contraseña.

Resultado

El acceso de todas las empresas pasa por una sola pieza extensible y los usuarios heredados entraron sin cambiar su contraseña.

Qué haría distinto

Cubrir las extensiones con tests de integración sobre un proveedor de identidad real en contenedor, en lugar de solo pruebas unitarias; y definir desde el principio la fecha de apagado del camino heredado, con una métrica que cuente cuántos logins siguen usándolo.

  • Java 21
  • Keycloak SPI
  • FreeMarker
  • JDBC multi-motor
  • Virtual Threads
  • OAuth2 / OIDC / JWT

Resiliencia

Resiliencia como librería y como servicio

Una librería con una sola operación pública y un gateway dedicado para que la degradación de un sistema heredado no tumbe lo que no tiene nada que ver.

Contexto

La plataforma nueva convive con sistemas heredados que atienden operaciones críticas y cuya latencia y disponibilidad no se controlan.

Problema

  • Cuando uno de esos sistemas se degradaba, el fallo se propagaba: los hilos de los servicios llamantes se quedaban esperando y las colas de peticiones crecían.
  • Cada servicio resolvía el problema a su manera, o no lo resolvía.

Mi rol

Autor único de la librería y del gateway.

Solución

  • Una librería con una sola operación pública: execute(instancia, llamada). Por dentro compone, en este orden, retry → time limiter → circuit breaker → bulkhead.
  • Publica métricas etiquetadas por instancia, ejecuta en hilos virtuales y traduce los fallos a excepciones propias desenvolviendo los wrappers de ejecución asíncrona.
  • La política es un record que valida su propia configuración al construirse: una configuración incoherente falla al arrancar, no en producción.
  • Un gateway dedicado para el tráfico hacia los sistemas heredados: rutas declaradas por configuración, un circuit breaker por combinación de sistema y operación, dos perfiles de política (normal de 3 s y de larga duración de 30 s) y fallback HTTP con Retry-After.

Decisiones y contrapartidas

El orden de composición importa
Retry dentro del time limiter, y ambos dentro del circuit breaker: así los reintentos cuentan dentro del presupuesto de tiempo y el breaker ve el resultado final, no cada intento.
Un HTTP 429 no es un fallo, es backpressure
Contarlo como error abre el circuito precisamente cuando el sistema remoto está pidiendo que bajes el ritmo. Se excluye del cómputo de fallos, pero no del retry.
Gateway dedicado en vez de breakers en cada consumidor
Añade un salto de red, pero centraliza la política, hace observable el estado de cada dependencia en un solo panel y permite cambiar umbrales sin desplegar los servicios de negocio.
Bulkhead con semáforo, no con pool de hilos
Con hilos virtuales el coste de un hilo ya no es el recurso escaso; lo que hay que limitar es la concurrencia contra el sistema remoto.

Resultado

Siete dependencias heredadas aisladas con política explícita, y tests de integración que verifican el comportamiento con el upstream caído.

Qué haría distinto

Exponer la política como configuración recargable en caliente y añadir ajuste automático de concurrencia según la latencia observada, en lugar de un tope fijo.

  • Java 21
  • Virtual Threads
  • Resilience4j
  • Spring Cloud Gateway
  • Spring Boot 4
  • Micrometer / Prometheus

Mensajería asíncrona

Procesamiento masivo de documentos con garantías

Sacar del camino síncrono la carga de paquetes con miles de documentos, sin duplicar registros cuando el cliente reintenta.

Contexto

Los clientes suben paquetes con miles de documentos electrónicos. El camino original era síncrono: subir, descomprimir, validar y registrar todo dentro de la misma petición HTTP.

Problema

  • Los paquetes grandes agotaban el timeout de la petición.
  • El proceso consumía memoria proporcional al tamaño del archivo.
  • Un reintento del cliente reprocesaba todo desde cero y duplicaba registros.

Mi rol

Creador de las dos piezas (productor y worker) y autor del 94 % del worker.

Solución

  • El productor guarda el paquete en almacenamiento de objetos en streaming hacia un fichero temporal con permisos restringidos, y publica en la cola solo el evento. La respuesta al cliente es inmediata.
  • El worker procesa por bloques acotados y, si el paquete tiene más, republica el evento con el resto: ningún mensaje individual tarda lo bastante como para agotar el ack.
  • Cada documento se registra en una tabla de idempotencia antes de confirmarse, así un reintento no duplica lo ya confirmado y sí reintenta lo que falló.
  • La cola tiene DLQ vinculada automáticamente con republicación, tres intentos, concurrencia acotada y prefetch 1 para que un consumidor lento no acapare mensajes.
  • Una segunda función consume la DLQ y un planificador de reencolado técnico recupera los casos que quedaron en un estado intermedio.

Decisiones y contrapartidas

El payload va al almacenamiento de objetos, no al mensaje
Mantiene los mensajes pequeños y el broker sano; a cambio hay que gestionar el ciclo de vida de los objetos y su limpieza.
Bloques con republicación en vez de un mensaje gigante
Cada unidad de trabajo cabe en un tiempo predecible; el precio es más mensajes y un contador de progreso que mantener.
Idempotencia en base de datos en vez de confiar en exactly-once del broker
Ningún broker da exactly-once de extremo a extremo; la única garantía real es que el consumidor sea idempotente.
prefetch 1 aunque baje el rendimiento máximo
Con mensajes de duración muy variable, un prefetch alto reparte mal el trabajo y alarga la cola de los lentos.

Resultado

La subida responde de inmediato y el procesamiento se reintenta sin duplicar lo ya confirmado.

Qué haría distinto

Publicar el progreso del paquete como eventos de dominio para que el frontend muestre avance real en vez de consultar estado; y medir la latencia por bloque con un histograma, no solo con contadores.

  • Spring Boot 4.1
  • Spring Cloud Stream
  • RabbitMQ
  • AWS S3
  • PostgreSQL
  • Virtual Threads

Capa de experiencia

Capa de experiencia multi-tenant sin estado en el edge

Un gateway por canal en vez de uno para todos, con el destino de cada petición resuelto por HTTP y cacheado sin provocar avalanchas.

Contexto

Varios canales consumen la misma plataforma: portal interno, canal público e integradores externos. Cada uno con su CORS, sus timeouts, su superficie expuesta y su forma de autenticarse.

Problema

  • Un único gateway para todos obligaba a mezclar políticas contradictorias: un CORS permisivo para el portal acaba siendo permisivo para todos.
  • El destino de cada petición depende de la empresa que la hace, información que vive en el servicio de configuración.

Mi rol

Creador de los tres gateways de experiencia.

Solución

  • Un gateway por audiencia, no uno para todas: cada uno declara sus rutas, su CORS, sus timeouts y su pool de conexiones con métricas.
  • Filtros globales propios: propagación de la cabecera de trazabilidad, resolución del destino downstream según la empresa y cabecera de contexto para los servicios de dominio.
  • El edge no abre base de datos: pregunta por HTTP al servicio de configuración.
  • La resolución tiene caché con TTL, tamaño máximo con desalojo de entradas vencidas y una sola consulta en vuelo por empresa, así una entrada que caduca en hora punta no dispara mil consultas idénticas.

Decisiones y contrapartidas

Gateway por canal en vez de uno solo
Más despliegues y más configuración, a cambio de que un cambio en el canal público no pueda abrir accidentalmente el interno.
Consultar la configuración por HTTP en lugar de compartir la base de datos
El gateway queda sin estado y escala horizontalmente sin conexiones a BD; el precio es una dependencia en caliente, mitigada con caché, timeouts y consulta única en vuelo.
Rutas por configuración en vez de por código
Añadir un destino no requiere recompilar; a cambio, la configuración crece y necesita revisión como si fuera código.

Resultado

Tres canales con políticas independientes y un edge sin estado, escalable horizontalmente.

Qué haría distinto

Extraer los tres gateways a una base común con los filtros compartidos como librería, en lugar de tenerlos replicados; y añadir rate limiting por empresa en el canal externo.

  • Spring Boot 4
  • Spring Cloud Gateway
  • WebClient reactivo
  • OAuth2 Resource Server
  • Micrometer / Prometheus

Micro-frontends

Micro-frontends federados con contrato de navegación

Que ocho módulos desarrollados por varias personas se presenten como una sola aplicación, sin un menú central que todos tengan que editar.

Contexto

Una plataforma con módulos muy distintos (documentos, analítica, facturación, identidad, administración, reportes, configuración, compras) desarrollados por varias personas, que debe presentarse al usuario como una sola aplicación.

Problema

  • Un frontend monolítico convertía cada cambio pequeño en un despliegue completo.
  • El menú de navegación era un archivo que todo el mundo tenía que editar: una fuente constante de conflictos.

Mi rol

Contribuyente principal de dos remotos completos y de la librería compartida de contexto de autenticación; el monorepo lo arrancó otra persona del equipo.

Solución

  • Cada remoto expone una función getNavItems() con los ítems de menú que aporta. El shell los fusiona, deduplica y filtra por los entitlements del usuario, calculados a partir del token.
  • El contexto de autenticación vive en una librería compartida con estado reactivo (signals) y expone consultas del tipo «¿tiene este permiso?», en lugar de que cada remoto interprete los claims por su cuenta.
  • Las dependencias se comparten como singleton con versión estricta, para que dos remotos no carguen dos copias del framework.
  • Cada remoto compila su propio CSS sobre una base de utilidades común, sin depender de que el shell cargue sus estilos.

Decisiones y contrapartidas

Contrato de navegación en lugar de configuración central
Cada equipo es dueño de su menú; el coste es que el orden del menú deja de ser trivial y hay que definir prioridades.
Compartir dependencias como singleton con versión estricta
Evita duplicar el framework en el bundle, pero obliga a subir versiones de forma coordinada en todo el monorepo.
Monorepo en vez de repositorios separados
Los refactors que atraviesan varios remotos son posibles en un solo cambio; a cambio, el repositorio crece y necesita disciplina de límites.

Resultado

Diez remotos desplegables de forma independiente, con el menú y el control de acceso derivados del token.

Qué haría distinto

Publicar el contrato de navegación como un tipo compartido versionado y validado en compilación, y añadir pruebas end-to-end que carguen el shell con los remotos reales para detectar roturas de contrato antes de desplegar.

  • Angular 21
  • Signals
  • Nx
  • Native Federation
  • TypeScript estricto
  • Tailwind CSS
  • keycloak-js
← Volver a los proyectos