Todo lo que hemos construido hasta ahora es una sola aplicación: un monolito probado, observable, configurable y contenerizado. Es una arquitectura perfectamente respetable y, para la red municipal de bicicletas de Ribalta, probablemente la correcta. Pero antes o después alguien plantea la pregunta: ¿y si facturación tuviera su propio ciclo de vida? ¿Y si el equipo de estaciones desplegara sin coordinarse con el de alquileres? ¿Y si hubiera que escalar solo la consulta de disponibilidad, que recibe cien veces más tráfico que el resto?

Esta lección responde a esas preguntas con honestidad. Veremos qué problema resuelven realmente los microservicios y cuál es su precio, cómo se decide dónde cortar, qué se rompe cuando cada servicio tiene su propia base de datos —los JOIN y las transacciones ACID de 04-07 dejan de existir— y qué patrones lo sustituyen. Recorreremos el ecosistema Spring Cloud señalando cuándo sobra, montaremos una pasarela mínima para CicloUrbana y terminaremos con una guía de migración progresiva y una lista de comprobación honesta. Aviso desde la primera línea: la respuesta correcta para un proyecto como CicloUrbana suele ser el monolito modular, y esta lección se toma en serio explicar por qué.

Contenido

  1. Qué es una arquitectura de microservicios
  2. Monolito, monolito modular y microservicios
  3. La ley de Conway y el contexto delimitado
  4. Descomponer CicloUrbana
  5. Base de datos por servicio: qué se rompe
  6. Consistencia eventual, sagas y el patrón outbox
  7. El ecosistema Spring Cloud
  8. Descubrimiento de servicios y API Gateway
  9. Configuración centralizada
  10. Autenticación entre servicios
  11. Comunicación síncrona frente a asíncrona
  12. Observabilidad distribuida
  13. Migración progresiva desde el monolito
  14. Errores Comunes y Consejos
  15. Ejercicios

  1. Qué es una arquitectura de microservicios

Un sistema de microservicios es un conjunto de servicios pequeños, desplegables de forma independiente, cada uno propietario de sus datos y comunicándose por la red. La definición importante está en las tres primeras palabras de esa frase: desplegables de forma independiente. Si para publicar un cambio en facturación hay que coordinar el despliegue con alquileres, no hay microservicios: hay un monolito distribuido, que combina los inconvenientes de ambos mundos.

Los problemas que resuelve son organizativos y operativos antes que técnicos:

  • Autonomía de equipos. Cinco equipos que despliegan cinco veces al día sin pisarse.
  • Escalado selectivo. Veinte réplicas de consulta de estaciones y dos de facturación.
  • Aislamiento de fallos. Que la pasarela de pagos se caiga y las bicicletas se sigan alquilando.
  • Libertad tecnológica. Un servicio de recomendación en Python junto a servicios en Java 21.
  • Ciclos de vida distintos. Un motor de tarifas que cambia cada semana frente a un catálogo de estaciones que cambia cada trimestre.

Y el precio, que rara vez se enuncia con la misma claridad: la complejidad se mueve del código a la operación. Una llamada a un método pasa a ser una llamada de red que puede tardar, fallar o llegar dos veces. Una transacción pasa a ser una coordinación entre servicios. Un NullPointerException pasa a ser una investigación en los logs de cuatro procesos. Nada de eso se paga una vez: se paga todos los días.

  1. Monolito, monolito modular y microservicios

Criterio Monolito Monolito modular Microservicios
Unidades de despliegue 1 1 N
Fronteras internas Difusas Explícitas y verificadas Físicas (procesos distintos)
Base de datos Una compartida Una, con esquemas por módulo Una por servicio
Transacciones ACID (04-07) ACID Consistencia eventual, sagas
Consultas entre áreas JOIN JOIN o API interna Llamadas de red o réplicas
Escalado Todo o nada Todo o nada Selectivo por servicio
Latencia interna Nanosegundos Nanosegundos Milisegundos, y variable
Autonomía de equipos Baja Media Alta
Coste operativo Bajo Bajo Muy alto
Depuración de un fallo Una pila de llamadas Una pila de llamadas Trazas distribuidas (09-06)
Requisitos previos Ninguno Disciplina CI/CD, observabilidad, automatización
Coste de equivocarse en la frontera Refactorización Refactorización Rediseño y migración de datos

Esa última fila es la razón del aviso inicial. En un monolito modular, mover una clase de un módulo a otro es una tarde de trabajo; entre microservicios, es una migración de datos, un contrato roto, dos despliegues coordinados y un periodo de convivencia. Y las fronteras casi nunca se aciertan a la primera, porque solo se aprende dónde están cuando el dominio ya se conoce bien.

De ahí la recomendación que sostiene la industria desde hace años: empieza por un monolito bien modularizado y extrae servicios cuando un dolor concreto lo justifique. Un dolor concreto es «el equipo de facturación no puede desplegar sin nosotros» o «necesito escalar la consulta de estaciones a veinte réplicas sin escalar el resto», no «los microservicios son más modernos».

  1. La ley de Conway y el contexto delimitado

«Las organizaciones que diseñan sistemas producen diseños que copian la estructura de comunicación de la propia organización.» — Melvin Conway, 1967

La consecuencia práctica es brutal: si el ayuntamiento de Ribalta tiene un solo equipo, los microservicios que diseñe acabarán acoplados, porque no hay ninguna frontera organizativa que los mantenga separados. Y al revés: si hay cuatro equipos con responsabilidades claras, la arquitectura tenderá a alinearse con ellos aunque se diseñe otra cosa. Diseñar la arquitectura es diseñar la organización.

Para decidir dónde cortar, el criterio que funciona viene del diseño guiado por el dominio (DDD): el contexto delimitado, una frontera dentro de la cual un término del negocio tiene un único significado. La prueba práctica es preguntar por las palabras:

  • Para alquileres, una «bicicleta» es un objeto con estado, batería y posición actual.
  • Para facturación, una «bicicleta» es apenas una referencia en una línea de factura.
  • Para mantenimiento, una «bicicleta» es un activo con historial de reparaciones y garantía.

Tres modelos distintos del mismo concepto, cada uno correcto en su contexto. Cuando un mismo término significa cosas distintas según quién lo use, ahí hay una frontera. Y al revés: si dos supuestos servicios necesitan constantemente el mismo modelo completo, no eran dos.

Dónde NO está la frontera, y son los dos errores más frecuentes:

Corte equivocado Por qué falla
Por capa técnica (servicio-controladores, servicio-repositorios) Cualquier cambio funcional toca los tres servicios: acoplamiento máximo con coste de red
Por tabla (servicio-estaciones, servicio-bicicletas, servicio-anclajes) Fragmenta un mismo concepto de negocio; cada operación necesita tres llamadas
Por entidad CRUD Produce servicios anémicos que solo sirven datos, con la lógica repartida por todas partes

Un microservicio debe poder responder a una petición de negocio completa por sí solo la mayor parte de las veces. Si para atender su caso de uso más común necesita llamar a otros tres, la frontera está mal puesta.

  1. Descomponer CicloUrbana

Aplicando el criterio anterior, la red de Ribalta admite cuatro candidatos razonables:

graph TB
    GW[API Gateway<br/>/api/v1/**]
    GW --> EST[servicio-estaciones<br/>estaciones, anclajes,<br/>ocupación, bicicletas]
    GW --> ALQ[servicio-alquileres<br/>alquiler, devolución,<br/>tarifas, incidencias]
    GW --> USU[servicio-usuarios<br/>ciudadanos, roles,<br/>autenticación]
    FAC[servicio-facturacion<br/>cobros, recibos,<br/>pasarela de pagos]
    ALQ -.->|evento AlquilerFinalizado| FAC
    ALQ -->|consulta disponibilidad| EST
    EST --> BDE[(BD estaciones)]
    ALQ --> BDA[(BD alquileres)]
    USU --> BDU[(BD usuarios)]
    FAC --> BDF[(BD facturación)]
Servicio Por qué es un contexto propio Ciclo de vida y escala
servicio-estaciones Modelo de infraestructura física; cambia poco Lectura intensiva: la app móvil consulta disponibilidad constantemente
servicio-alquileres El núcleo del negocio: iniciar, finalizar, tarificar Escritura intensiva, picos en hora punta
servicio-usuarios Identidad y autenticación, con normativa propia (RGPD) Estable, poco tráfico
servicio-facturacion Vocabulario contable, integra con la pasarela externa Asíncrono, tolera retrasos

Nótese que las bicicletas viven dentro de servicio-estaciones y no en un servicio propio: su ciclo de vida está atado al de la infraestructura, y separarlas obligaría a una llamada de red en cada consulta de disponibilidad, que es justo la operación más frecuente de todo el sistema. Es la aplicación directa de la regla del apartado anterior.

Y una observación incómoda: de los cuatro, solo servicio-facturacion tiene una justificación fuerte. Es el que integra con un tercero, el que tolera consistencia eventual por naturaleza, el que tiene el vocabulario más distinto y el que menos consultas cruzadas necesita. Si CicloUrbana tuviera que extraer un solo servicio, ese sería el candidato, y los otros tres podrían seguir siendo módulos del monolito durante años.

  1. Base de datos por servicio: qué se rompe

La regla no negociable de los microservicios es que cada servicio es dueño exclusivo de sus datos: nadie más los lee directamente. Compartir base de datos entre servicios los acopla en el peor punto posible —el esquema— y anula la independencia de despliegue: una migración de Flyway (04-08) pasaría a requerir la coordinación de todos.

Y esa regla rompe dos cosas que llevamos usando todo el curso:

Los JOIN desaparecen. Esta consulta perfectamente natural del monolito:

SELECT a.id, u.nombre, e.nombre AS estacion, a.importe
FROM alquileres a
JOIN usuarios u   ON u.id = a.usuario_id
JOIN estaciones e ON e.id = a.estacion_origen_id
WHERE a.fecha_inicio >= :desde;

deja de ser posible: usuarios y estaciones están en otras bases de datos. Las alternativas, con su precio:

Alternativa Cómo funciona Coste
Composición en el cliente El servicio pide los nombres a los otros dos N+1 sobre la red (04-04, ahora con latencia)
Composición en la pasarela La pasarela agrega las respuestas Lógica de negocio en la infraestructura
Réplica local de solo lectura Cada servicio guarda copia de los datos ajenos que necesita Consistencia eventual, duplicación deliberada
CQRS con vista materializada Un servicio de consulta construye vistas desde los eventos Complejidad alta, muy eficaz en lectura

La tercera es la más usada: servicio-alquileres guarda el nombreEstacion junto al alquiler, actualizándolo cuando llega un evento EstacionRenombrada. Es duplicación deliberada, algo que en una base de datos normalizada sería un error y aquí es la solución correcta.

Las transacciones ACID desaparecen. Todo el módulo 4 se apoyaba en que @Transactional garantizaba atomicidad: si fallaba el cobro, se deshacía el alquiler. Con dos bases de datos, eso ya no existe. Las transacciones distribuidas de dos fases (XA) técnicamente existen, pero en la práctica se descartan: bloquean recursos en varios sistemas, no escalan y una pasarela de pagos externa no participa en ellas.

  1. Consistencia eventual, sagas y el patrón outbox

Lo que sustituye a la transacción distribuida es la consistencia eventual: el sistema pasa por estados intermedios inconsistentes y converge. Aplicado a Ribalta: durante unos segundos, un alquiler está finalizado y aún no cobrado. Eso no es un defecto técnico, es una decisión de negocio que hay que tomar conscientemente: ¿es aceptable que un ciudadano vea su alquiler cerrado antes de que aparezca el cargo? Casi siempre sí. ¿Es aceptable que una bicicleta figure disponible cuando ya no lo está? Casi nunca.

Una saga es una secuencia de transacciones locales donde cada paso publica un evento que dispara el siguiente, y cada uno tiene su compensación —no hay rollback, hay una acción que deshace—.

sequenceDiagram
    participant C as Ciudadano
    participant A as servicio-alquileres
    participant B as Bus de eventos
    participant F as servicio-facturacion
    participant P as Pasarela de pagos
    C->>A: POST /alquileres/42/finalizar
    A->>A: cierra alquiler (transacción local) + outbox
    A-->>C: 200 OK — alquiler finalizado
    A->>B: AlquilerFinalizado(42, 4,80 €)
    B->>F: entrega el evento
    F->>P: cobra 4,80 €
    alt cobro correcto
        F->>B: CobroRealizado(42)
        B->>A: marca el alquiler como cobrado
    else cobro rechazado
        F->>B: CobroRechazado(42, saldo insuficiente)
        B->>A: compensación: marca deuda pendiente y bloquea nuevos alquileres
    end

Coreografiada frente a orquestada:

Coreografiada Orquestada
Cómo avanza Cada servicio reacciona a eventos Un coordinador dice a cada uno qué hacer
Acoplamiento Bajo Medio: todos dependen del coordinador
Visibilidad del flujo Ninguna: no está escrito en ningún sitio Explícita, en un solo lugar
Depurar Difícil Más fácil
Cuándo usarla 2-3 pasos sencillos Flujos largos, con muchas compensaciones

La coreografiada es la del diagrama y la adecuada para el flujo de CicloUrbana. Su punto débil es serio: el flujo completo no está escrito en ningún archivo; para saber qué pasa al finalizar un alquiler hay que leer los escuchadores de cuatro servicios. En cuanto la saga pasa de tres o cuatro pasos, un orquestador explícito compensa su acoplamiento.

El patrón outbox resuelve un problema sutil pero fatal. Este código está mal:

@Transactional
public void finalizar(Long alquilerId) {
    alquiler.finalizar();                        // escribe en PostgreSQL
    broker.publicar(new AlquilerFinalizado(...)); // escribe en Kafka  ← problema
}

Son dos sistemas distintos sin transacción común. Si el commit de PostgreSQL falla después de publicar, existe un evento de un alquiler que no se cerró; si el broker falla tras el commit, el alquiler está cerrado y nadie lo cobrará jamás. La solución es escribir el evento en la misma transacción y en la misma base de datos, en una tabla outbox, y publicarlo después:

@Transactional
public void finalizar(Long alquilerId) {
    Alquiler alquiler = repositorio.findById(alquilerId).orElseThrow();
    alquiler.finalizar(Instant.now(reloj));
    outboxRepositorio.save(new MensajeOutbox(
            "AlquilerFinalizado", alquilerId, json.escribir(evento)));   // misma transacción
}

Un proceso aparte —una tarea @Scheduled con ShedLock de 07-03, o un conector de captura de cambios como Debezium— lee la tabla y publica. Como puede publicar dos veces si falla justo después de enviar, el consumidor debe ser idempotente: eso es lo que convierte una entrega «al menos una vez» en un efecto «exactamente una vez».

  1. El ecosistema Spring Cloud

Pieza Qué resuelve Alternativa en Kubernetes (08-04)
Config Server Configuración centralizada y versionada ConfigMap y Secret
Eureka / Consul Descubrimiento: dónde está cada instancia Service + DNS interno
Spring Cloud Gateway Punto de entrada, enrutado, filtros Ingress o una malla de servicios
OpenFeign Cliente HTTP declarativo RestClient o @HttpExchange (07-06)
Circuit Breaker Abstracción sobre Resilience4j La misma biblioteca, sin la abstracción
Micrometer Tracing (antes Sleuth) Trazas distribuidas Igual, más una malla de servicios
Stream / Bus Abstracción sobre Kafka o RabbitMQ El cliente nativo del broker

Y aquí va la parte que rara vez se dice. Buena parte de Spring Cloud nació antes de que Kubernetes fuera el estándar, para resolver en la aplicación problemas que hoy resuelve la plataforma. Si CicloUrbana se despliega en Kubernetes:

  • Eureka sobra: un Service de Kubernetes ya da un nombre DNS estable con balanceo. Añadir un registro de servicios propio duplica el mecanismo.
  • Config Server suele sobrar: los ConfigMap y los Secret montados como ficheros, leídos con spring.config.import: configtree: (07-02), hacen el trabajo sin un servicio más que mantener.
  • La pasarela puede sobrar: un Ingress enruta por ruta y por host. Sigue teniendo sentido cuando hace falta lógica —agregación de respuestas, transformación, limitación de tasa por usuario autenticado—.

Lo que no sobra en ningún caso: la resiliencia (07-06), la observabilidad distribuida (09-06) y la mensajería. La regla: no añadas una pieza de Spring Cloud sin poder nombrar el problema concreto que resuelve y comprobar que tu plataforma no lo resuelve ya. Cada componente es un servicio más que desplegar, monitorizar, actualizar y que puede caerse.

  1. Descubrimiento de servicios y API Gateway

El descubrimiento responde a «¿en qué dirección está servicio-alquileres ahora mismo?», una pregunta que no tiene respuesta fija porque las instancias aparecen, desaparecen y cambian de IP. Un registro —Eureka, Consul o el DNS de Kubernetes— mantiene esa lista, y el cliente pide «una instancia sana de servicio-alquileres» en lugar de una IP.

La pasarela es el único punto de entrada desde el exterior. Concentra lo que no tiene sentido repetir en cada servicio: enrutado, terminación TLS, CORS, limitación de tasa y una primera validación del token.

spring:
  cloud:
    gateway:
      routes:
        - id: estaciones
          uri: lb://servicio-estaciones            # lb: resuelto por el descubrimiento
          predicates:
            - Path=/api/v1/estaciones/**
          filters:
            - name: CircuitBreaker
              args: { name: cbEstaciones, fallbackUri: forward:/respaldo/estaciones }

        - id: alquileres
          uri: lb://servicio-alquileres
          predicates:
            - Path=/api/v1/alquileres/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 20
                redis-rate-limiter.burstCapacity: 40
      default-filters:
        - AddRequestHeader=X-Traza-Id, ${trazaId}   # coherente con FiltroTraza (03-06)

Tres detalles. lb:// delega la resolución en el descubrimiento y reparte entre instancias sanas. Los predicates deciden qué peticiones entran en cada ruta —por ruta, cabecera, método, host o incluso franja horaria—. Y Spring Cloud Gateway es reactivo, construido sobre WebFlux: no se puede mezclar con spring-boot-starter-web en el mismo proyecto, y su código de filtros no debe bloquear.

Lo que la pasarela no debe hacer: lógica de negocio. Una pasarela que decide si un ciudadano puede alquilar se convierte en un monolito encubierto por el que pasa todo el equipo, y en un punto único de fallo con despliegues coordinados. Enruta, protege y observa; no decide.

  1. Configuración centralizada

Con veinte servicios y cuatro entornos, la configuración se dispersa. Spring Cloud Config Server la centraliza en un repositorio Git y la sirve por HTTP:

# Config Server
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/ayuntamiento-ribalta/ciclourbana-config
          search-paths: '{application}'
# Cada servicio cliente
spring:
  application:
    name: servicio-alquileres
  config:
    import: optional:configserver:http://config:8888

El cliente pide al arrancar servicio-alquileres con sus perfiles activos (07-02) y recibe la configuración combinada, versionada en Git con historial y revisiones de código. Con Spring Cloud Bus, un cambio puede además refrescarse en caliente sin reiniciar.

Ventajas reales: una sola fuente de verdad, historial completo de quién cambió qué y cuándo, y cifrado de secretos con {cipher}. Inconvenientes igual de reales: un servicio más que mantener y que, si cae, impide arrancar a todos los demás; latencia añadida al arranque; y la tentación de meter ahí cosas que deberían ser código. Como se decía en el apartado 7: en Kubernetes, los ConfigMap suelen bastar.

  1. Autenticación entre servicios

El JWT de 05-04 sigue siendo la pieza central, pero cambia el reparto de responsabilidades:

graph LR
    C[Ciudadano] -->|JWT| GW[Gateway<br/>valida firma y caducidad]
    GW -->|propaga el JWT| ALQ[servicio-alquileres<br/>valida otra vez]
    ALQ -->|propaga el JWT| EST[servicio-estaciones<br/>valida otra vez]

Cada servicio valida el token por su cuenta, y esto no es redundancia inútil: es el principio de confianza cero. Si servicio-estaciones confiara en que la pasarela ya validó, cualquiera que alcanzara ese servicio desde dentro de la red —otro servicio comprometido, un atacante que ya entró— tendría acceso total. La validación es barata: comprobar una firma HS256 son microsegundos y no requiere llamar a nadie.

El patrón se llama token relay: la petición saliente lleva el mismo Authorization que llegó, y cada servicio actúa como resource server (spring-boot-starter-oauth2-resource-server en un escenario con OIDC, o el FiltroAutenticacionJwt propio de 05-04). La propagación de la cabecera se implementa con un interceptor del cliente HTTP, que es exactamente lo que construiremos en 07-06.

Para llamadas sin usuario —una tarea programada de facturación que consulta alquileres— no hay token que propagar. Ahí se usa el flujo de credenciales de cliente: el servicio obtiene su propio token con su identidad, no con la de ningún ciudadano. Y para autenticación mutua entre servicios, mTLS, que suele proporcionar la malla de servicios sin tocar el código.

  1. Comunicación síncrona frente a asíncrona

Síncrona (REST, gRPC) Asíncrona (RabbitMQ, Kafka)
Quién espera El llamante, bloqueado Nadie
Acoplamiento temporal Ambos deben estar vivos a la vez El receptor puede estar caído
Propagación de fallos Alta: una caída se encadena Baja: los mensajes esperan en la cola
Latencia percibida Suma de todas las llamadas Respuesta inmediata
Consistencia Inmediata Eventual
Depuración Sencilla: hay una pila Difícil: hay que seguir mensajes
Cuándo usarla Necesito la respuesta para continuar Solo necesito comunicar que algo pasó

La última fila es el criterio, y es más útil que cualquier otra consideración. Aplicado a CicloUrbana:

  • Iniciar un alquiler necesita saber si la bicicleta está disponible → llamada síncrona de servicio-alquileres a servicio-estaciones. No se puede continuar sin la respuesta.
  • Finalizar un alquiler no necesita esperar al cobro → evento asíncrono AlquilerFinalizado que servicio-facturacion consume cuando puede. Si facturación está caída una hora, los ciudadanos siguen alquilando y devolviendo bicicletas, y los cobros se procesan al volver.

Ese segundo caso es la mejor ilustración del valor de la mensajería: convierte la caída de un servicio en un retraso en lugar de en una avería. El precio es la consistencia eventual y la obligación de que el consumidor sea idempotente, porque los brokers garantizan «al menos una vez»: si AlquilerFinalizado(42) llega dos veces, no puede cobrarse dos veces. La forma habitual es una tabla de identificadores ya procesados, consultada dentro de la misma transacción del consumidor.

  1. Observabilidad distribuida

En el monolito, un fallo deja una pila de llamadas completa. Con cuatro servicios, «finalizar un alquiler tarda ocho segundos» es una pregunta sin respuesta hasta que se sabe en cuál de los cuatro se van esos segundos. Por eso la observabilidad distribuida no es un extra: es un requisito previo.

Las tres piezas, ya vistas o por ver:

Pieza Qué aporta Dónde se trata
Trazas Un traceId que atraviesa todos los servicios y mide cada tramo 09-06
Métricas Latencia, tasa de error y saturación por servicio 09-03 y 09-04
Logs correlacionados Todas las líneas de una operación, en todos los servicios 09-05

Micrometer Tracing propaga el contexto por las cabeceras traceparent del estándar W3C, e integra el identificador en el MDC, exactamente igual que el FiltroTraza de 03-06 hacía dentro de un proceso. Y el /actuator/health de 07-01 pasa a tener un papel nuevo: con cuatro servicios, el estado agregado del sistema es la unión de cuatro sondas, y el orquestador retira instancias por su cuenta.

El criterio de decisión que se deriva: si el equipo no tiene hoy trazas distribuidas ni logs centralizados, no está preparado para microservicios. No es una cuestión de madurez abstracta: sin esas herramientas, el primer incidente en producción es irresoluble.

  1. Migración progresiva desde el monolito

Nadie reescribe un monolito en marcha. El camino que funciona tiene tres etapas, y las dos primeras aportan valor aunque nunca se llegue a la tercera.

Etapa 1: modularizar por dentro. Reorganizar el código en módulos con fronteras explícitas, que es lo que ya empezamos en 01-04 con los paquetes .estaciones, .alquileres, .usuarios, .seguridad y .comun. La diferencia es que ahora las fronteras se verifican: nadie accede a las clases internas de otro módulo, la comunicación pasa por su API pública o por eventos, y cada módulo tiene su propio esquema en la base de datos.

Spring Modulith convierte esa disciplina en algo comprobable: define qué es un módulo, permite exponer solo lo público, y ofrece una prueba que falla la construcción si un módulo importa las tripas de otro. Con ella, la arquitectura deja de depender de la vigilancia en las revisiones de código:

class ModularidadTest {
    @Test
    void losModulosRespetanSusFronteras() {
        ApplicationModules.of(CicloUrbanaApplication.class).verify();
    }
}

Etapa 2: extraer el primer servicio con el patrón strangler fig. El nombre viene de la higuera estranguladora, que crece alrededor de un árbol hasta sustituirlo. Aplicado a CicloUrbana, para extraer facturación:

  1. Poner una pasarela delante del monolito, que de momento lo enruta todo hacia él.
  2. Crear servicio-facturacion con su base de datos, que duplica la funcionalidad de cobro.
  3. Enviarle una copia de los eventos y comparar resultados sin usarlos aún.
  4. Cuando coincidan durante semanas, mover en la pasarela el tráfico de facturación al servicio nuevo.
  5. Borrar el código de facturación del monolito.

La clave está en los pasos 3 y 4: hay un periodo de funcionamiento en paralelo en el que se puede volver atrás cambiando una ruta. Sin esa red, la extracción es un salto sin paracaídas.

Etapa 3: repetir solo cuando duela. Cada extracción posterior debe justificarse con un dolor concreto y medible.

La lista de comprobación honesta. Si no puedes responder «sí» a la mayoría, la respuesta es el monolito modular:

  • [ ] ¿Hay más de un equipo que se estorba al desplegar?
  • [ ] ¿La entrega continua está automatizada, de commit a producción, y tarda minutos?
  • [ ] ¿Hay trazas distribuidas y logs centralizados funcionando hoy?
  • [ ] ¿La infraestructura se crea de forma automatizada, sin pasos manuales?
  • [ ] ¿El equipo puede operar cuatro servicios, con guardias y alertas?
  • [ ] ¿Se conocen las fronteras del dominio lo bastante bien como para acertar?
  • [ ] ¿Hay un dolor concreto que el monolito modular no puede resolver?
  • [ ] ¿Se acepta la consistencia eventual en las operaciones afectadas?

Para CicloUrbana, con un equipo pequeño y un dominio estable, la respuesta sincera es no todavía, y eso es una conclusión legítima de esta lección.

Errores Comunes y Consejos

Empezar por microservicios sin conocer el dominio. Las fronteras se aciertan cuando el dominio se conoce, y eso llega después de construirlo.

Compartir la base de datos entre servicios. Es el monolito distribuido: el acoplamiento del monolito con la latencia de la red y sin sus transacciones.

Cortar por capas o por tablas. Produce servicios que no pueden atender una petición sin llamar a otros tres.

Servicios que solo despliegan juntos. Si hay que coordinar despliegues, la independencia —el único beneficio real— no existe.

Publicar el evento fuera de la transacción. Sin outbox, o hay eventos de cosas que no ocurrieron o hay cosas ocurridas sin evento.

Consumidores no idempotentes. Los brokers entregan «al menos una vez»: un cobro duplicado es cuestión de tiempo.

Añadir Spring Cloud por completo «porque es lo que se usa». Cada pieza es un servicio más que operar; en Kubernetes, varias ya están resueltas.

Poner lógica de negocio en la pasarela. Se convierte en un monolito encubierto y en un punto único de fallo.

Consejo: empieza por el monolito modular y verifica sus fronteras con Spring Modulith. Aporta el 80 % del beneficio con el 5 % del coste.

Consejo: extrae primero el servicio menos acoplado, el que integra con un tercero o tolera consistencia eventual. En CicloUrbana, facturación.

Consejo: monta la observabilidad antes que el primer servicio. El día que la necesites será tarde para instalarla.

Consejo: escribe los contratos antes que el código. OpenAPI (03-07) para lo síncrono y un esquema versionado para los eventos. Un contrato roto entre servicios se descubre en producción.

Ejercicios

Ejercicio 1: decidir la frontera

El ayuntamiento de Ribalta quiere añadir a CicloUrbana un sistema de abonos anuales: un ciudadano paga una cuota, obtiene tarifa reducida durante un año, y el abono debe validarse al iniciar cada alquiler. Decide si esto justifica un servicio propio, un módulo dentro de servicio-alquileres o un módulo del monolito. Argumenta con los criterios de los apartados 3 y 4, y describe cómo se comunicaría con el resto en cada caso.

Ejercicio 2: la saga del alquiler con abono

Diseña el flujo completo, como saga coreografiada, de «finalizar alquiler con cobro y actualización del consumo del abono», con tres participantes (servicio-alquileres, servicio-facturacion, servicio-abonos). Dibuja el diagrama, define los eventos con sus datos, indica las compensaciones de cada paso y explica cómo garantizas que un cobro no se duplique si el evento llega dos veces.

Ejercicio 3: revisar una propuesta de arquitectura

Un consultor propone al ayuntamiento esta arquitectura. Encuentra los problemas y propón una alternativa.

«Dividimos CicloUrbana en seis microservicios: ms-controladores (toda la capa REST), ms-negocio (todos los servicios), ms-datos (todos los repositorios), ms-estaciones, ms-bicicletas y ms-anclajes. Los seis comparten la base de datos PostgreSQL actual para no duplicar información y poder seguir usando los JOIN. Se despliegan juntos desde la misma canalización para garantizar que las versiones son coherentes. Usamos Eureka, Config Server, Gateway, Feign y Bus, todo el ecosistema Spring Cloud, sobre Kubernetes.»

Soluciones

Solución 1

La recomendación es un módulo dentro de servicio-alquileres —o, si CicloUrbana sigue siendo un monolito, un módulo más—. El razonamiento, criterio a criterio:

Prueba del vocabulario. Un «abono» solo tiene significado en el contexto del alquiler: es un modificador de tarifa. No aparece con otro sentido en estaciones ni en mantenimiento. No hay dos modelos distintos del mismo término, luego no hay frontera de contexto.

Prueba de la petición completa. La operación más frecuente —iniciar un alquiler— necesita siempre consultar si el ciudadano tiene abono vigente. Un servicio aparte convertiría cada inicio de alquiler en una llamada de red adicional en el camino crítico, con su latencia, su posibilidad de fallo y su cortacircuitos (07-06). Eso es exactamente lo que el apartado 3 describe como frontera mal puesta.

Prueba del ciclo de vida. Las reglas de los abonos cambian con la misma cadencia que las tarifas, y ambas las decide la misma área del ayuntamiento. Nada sugiere ciclos de despliegue independientes.

Prueba organizativa. No hay un equipo de abonos. Por la ley de Conway, un servicio sin equipo propio acaba acoplado a quien lo mantenga de hecho.

Dónde sí encaja un servicio propio es en el cobro de la cuota anual, que ya pertenece a servicio-facturacion: emitir el recibo del abono es el mismo vocabulario contable que cobrar un alquiler, tolera consistencia eventual e integra con la pasarela externa.

Comunicación resultante. Dentro de servicio-alquileres, el módulo de abonos expone una API interna (AbonoService.tarifaVigente(usuarioId, fecha)) invocada como una llamada a método, sin red y dentro de la misma transacción. Hacia fuera publica dos eventos: AbonoContratado, que servicio-facturacion consume para emitir el recibo, y AbonoCaducado, que sirve para notificar al ciudadano. Si algún día hubiera un equipo dedicado y las reglas se volvieran mucho más complejas, la extracción sería sencilla precisamente porque el módulo ya tiene una frontera explícita: es la etapa 1 del apartado 13 haciendo su trabajo.

Solución 2

sequenceDiagram
    participant C as Ciudadano
    participant A as servicio-alquileres
    participant B as Bus
    participant AB as servicio-abonos
    participant F as servicio-facturacion
    C->>A: POST /api/v1/alquileres/42/finalizar
    A->>A: cierra alquiler + calcula 4,80 € + outbox (una transacción)
    A-->>C: 200 OK
    A->>B: AlquilerFinalizado(id=42, usuario=7, importe=4.80, minutos=32)
    B->>AB: entrega
    AB->>AB: descuenta 32 min del abono; quedan 0,00 € a cobrar
    AB->>B: ConsumoAbonoAplicado(42, importeFinal=0.00)
    B->>F: entrega
    alt importeFinal = 0
        F->>B: CobroNoNecesario(42)
    else importeFinal > 0 y cobro correcto
        F->>B: CobroRealizado(42, 4.80)
    else cobro rechazado
        F->>B: CobroRechazado(42, "saldo insuficiente")
        B->>AB: compensación: devuelve los 32 min al abono
        B->>A: compensación: marca deuda pendiente y bloquea nuevos alquileres
    end

Los eventos y sus datos. Cada uno lleva eventoId (UUID único), ocurridoEn (instante) y version del esquema, además de su carga: AlquilerFinalizado con alquilerId, usuarioId, importeBruto y minutos; ConsumoAbonoAplicado con alquilerId, minutosConsumidos e importeFinal; CobroRealizado y CobroRechazado con alquilerId, importe y, este último, motivo.

Las compensaciones, que sustituyen al rollback:

Paso Compensación si falla lo posterior
Cierre del alquiler No se deshace: el ciudadano devolvió la bicicleta y eso es un hecho. Se marca PENDIENTE_DE_PAGO
Consumo del abono MinutosDevueltosAlAbono(42, 32): se reintegran los minutos
Cobro Un cobro correcto se compensa con una devolución, no con un borrado

La primera fila encierra la lección más importante de las sagas: no todo se puede deshacer. La compensación no es volver al estado anterior, sino llevar el sistema a un estado consistente nuevo, que aquí es «alquiler finalizado con deuda pendiente».

La idempotencia, que es lo que hace segura la entrega «al menos una vez». En servicio-facturacion:

@Transactional
public void alRecibir(ConsumoAbonoAplicado evento) {
    if (procesadosRepositorio.existsById(evento.eventoId())) {
        return;                                    // ya tratado: se ignora
    }
    procesadosRepositorio.save(new EventoProcesado(evento.eventoId(), Instant.now(reloj)));
    if (evento.importeFinal().signum() > 0) {
        pasarela.cobrar(evento.alquilerId(), evento.importeFinal());
    }
}

Tres detalles que lo hacen correcto: la comprobación y el registro van en la misma transacción que el efecto, de modo que no puede quedar registrado sin cobrar ni cobrado sin registrar; la clave primaria de EventoProcesado garantiza la unicidad en la base de datos, así que dos consumidores concurrentes no pasan los dos; y la tabla se purga periódicamente con una tarea programada de 07-03, porque no puede crecer indefinidamente. Como salvaguarda adicional, la llamada a la pasarela lleva una clave de idempotencia propia (el alquilerId), de modo que incluso si CicloUrbana pidiera dos cobros, la pasarela solo aplicaría uno.

Solución 3

La propuesta acumula prácticamente todos los errores de la lección. Los problemas, ordenados por gravedad:

# Problema Por qué es grave
1 Corte por capas (ms-controladores, ms-negocio, ms-datos) Cualquier cambio funcional —añadir un campo a una estación— toca los tres servicios y exige tres despliegues coordinados. Es acoplamiento máximo pagando latencia de red
2 Base de datos compartida Anula la independencia: una migración de Flyway afecta a los seis. Es la definición de monolito distribuido
3 Despliegue conjunto El único beneficio real de los microservicios era desplegar por separado; si se despliegan juntos, se ha pagado todo el coste sin ninguna ventaja
4 Corte por tablas (ms-estaciones, ms-bicicletas, ms-anclajes) Fragmenta un solo concepto de negocio; consultar la disponibilidad de «Plaza Mayor» exigiría tres llamadas de red
5 Solapamiento de responsabilidades ms-negocio y ms-estaciones se pisan: no está claro dónde vive la lógica de estaciones
6 Todo Spring Cloud sobre Kubernetes Eureka duplica los Service, Config Server duplica los ConfigMap: dos servicios más que operar y que pueden caerse, sin resolver nada nuevo
7 No se menciona la observabilidad Sin trazas distribuidas, el primer incidente entre seis servicios es irresoluble
8 No hay justificación No se nombra ningún dolor concreto que el monolito no resuelva

La alternativa. Etapa 1: dejar CicloUrbana como un solo despliegue, reorganizado en módulos verificados con Spring Modulith —estaciones (con bicicletas y anclajes dentro, porque son un solo concepto), alquileres, usuarios, facturacion— cada uno con su esquema en PostgreSQL y comunicándose por API pública o eventos de Spring. Esto da fronteras reales, límites comprobados por la construcción y coste operativo cero.

Etapa 2: montar la observabilidad —trazas, métricas y logs centralizados (09-03 a 09-06)— y la entrega continua (08-05). Son requisito previo, no consecuencia.

Etapa 3: si y solo si aparece un dolor concreto, extraer servicio-facturacion con el patrón strangler fig, con su propia base de datos, comunicándose por eventos con outbox y consumo idempotente. Un servicio, no seis.

De Spring Cloud, sobre Kubernetes, quedaría solo lo que la plataforma no da: Resilience4j para la resiliencia (07-06), Micrometer Tracing para las trazas (09-06) y, si la mensajería lo justifica, el cliente del broker. Ni Eureka, ni Config Server, ni Bus.

El resumen que se le devolvería al consultor: la propuesta tiene el coste operativo de seis microservicios, el acoplamiento de un monolito y ninguno de los beneficios de ninguna de las dos arquitecturas.

Conclusión

Esta lección ha sido tanto sobre cuándo no usar microservicios como sobre cómo usarlos. Sabes que su definición operativa es desplegable de forma independiente, y que si dos servicios deben desplegarse juntos no son microservicios sino un monolito distribuido. Tienes la tabla que compara monolito, monolito modular y microservicios en once dimensiones, con la fila decisiva: equivocarse en una frontera cuesta una tarde en un módulo y una migración de datos entre servicios. Conoces la ley de Conway y sus consecuencias, y el criterio que sí funciona para decidir dónde cortar —el contexto delimitado, detectado por la prueba del vocabulario— junto con los dos cortes que nunca funcionan: por capa técnica y por tabla.

Has descompuesto CicloUrbana en servicio-estaciones, servicio-alquileres, servicio-usuarios y servicio-facturacion, entendiendo por qué las bicicletas se quedan dentro de estaciones y por qué, de los cuatro, solo facturación tiene una justificación fuerte. Sabes qué se rompe con una base de datos por servicio —los JOIN y las transacciones ACID de 04-07— y qué lo sustituye: composición, réplicas locales de solo lectura como duplicación deliberada, consistencia eventual, sagas coreografiadas y orquestadas con sus compensaciones, y el patrón outbox que evita el fallo sutil de escribir en la base de datos y en el broker sin transacción común, con la idempotencia del consumidor como pieza que convierte «al menos una vez» en «exactamente una vez».

Conoces el ecosistema Spring Cloud pieza a pieza y, más importante, cuándo sobra: en Kubernetes, Eureka duplica los Service y Config Server duplica los ConfigMap. Sabes qué aportan el descubrimiento y la pasarela, has visto la configuración mínima de Spring Cloud Gateway enrutando /api/v1/estaciones/** y /api/v1/alquileres/**, y tienes claro que la pasarela enruta y protege pero no decide. Entiendes el token relay y por qué cada servicio valida el JWT por su cuenta, el criterio para elegir entre comunicación síncrona y asíncrona —¿necesito la respuesta para continuar?—, y por qué la observabilidad distribuida no es un extra sino un requisito previo. Y tienes la guía de migración: modularizar primero con Spring Modulith verificando las fronteras en la construcción, extraer después el primer servicio con el patrón strangler fig y su periodo de funcionamiento en paralelo, y la lista de comprobación que para CicloUrbana da hoy un «no todavía» perfectamente legítimo.

Queda pendiente lo más práctico de todo. Hemos dicho que servicio-alquileres llama a servicio-estaciones, que hay que propagar el JWT y el identificador de traza, que hace falta un cortacircuitos en la pasarela y que la caída de un servicio no debe encadenarse. Nada de eso lo hemos escrito todavía, y sigue haciendo falta aunque CicloUrbana no se divida nunca: en cuanto la aplicación llama a la pasarela de pagos de Ribalta —un sistema externo que puede estar lento, caído o devolver errores—, todos esos problemas aparecen exactamente igual. La siguiente lección, Comunicación entre Servicios y Tolerancia a Fallos, los resuelve con código: RestClient y las interfaces declarativas, tiempos de espera que no se pueden olvidar, interceptores que propagan la traza y el token, y Resilience4j con sus reintentos, cortacircuitos, limitadores, mamparos y degradación elegante, todo probado con un servidor simulado que finge estar lento, caído y roto.

Curso de Spring Boot

Módulo 1: Introducción a Spring Boot

Módulo 2: Conceptos Básicos de Spring Boot

Módulo 3: Construyendo Servicios Web RESTful

Módulo 4: Acceso a Datos con Spring Boot

Módulo 5: Seguridad en Spring Boot

Módulo 6: Pruebas en Spring Boot

Módulo 7: Funciones Avanzadas de Spring Boot

Módulo 8: Despliegue de Aplicaciones Spring Boot

Módulo 9: Rendimiento y Monitoreo

Módulo 10: Mejores Prácticas y Consejos

© Copyright 2026. Todos los derechos reservados