
Cuando aparecieron los microservicios, lo hicieron con el objetivo de resolver algunos de los problemas que siempre han amenazado a la ingeniería del software: la separación de tareas (modularidad) y la reutilización. Su función principal es dividir el producto en elementos independientes y reutilizables, cada uno de los cuales implementa una funcionalidad (y sólo una) y son, por tanto, fáciles de mantener y de desplegar porque se sabe que los cambios en un elemento no afectarían a otro.
Eran el último escalón en la solución al problema de la separación del código que implementa cada tarea. Antes de ellos, las buenas prácticas consistían en estructurar los programas en módulos que agrupaban una funcionalidad similar. Se separaba el código en módulos que se cargaban en el momento de la compilación (librerías). El escalón anterior (la orientación a objetos) ya maximizaba la reutilización sin preocuparse por la implementación del objeto en sí.
Van un paso más allá. Además de separar la funcionalidad en el momento del diseño y la creación del código, también separa la ejecución porque cada microservicio es un producto independiente y, si somos puristas, sin compartir nada con otro microservicio, ni siquiera su base de datos.
Desarrollar una aplicación con el paradigma de los microservicios produce un resultado teórico óptimo: la funcionalidad está claramente identificada en unidades de operación que se ejecutan de forma independiente, sin acoplamiento, no existe efecto mariposa y cada microservicio puede mantenerse de forma independiente sin afectar al resto. No obstante, ello implica crear una constelación de elementos que se despliegan de manera independiente, sin compartir datos, que se comunican a través de eventos y llamadas de red.
Monolitos modulares
Para romper totalmente con los modelos anteriores, se crea el concepto (peyorativo) de monolito. Un monolito es ese producto único, que contiene toda la funcionalidad y que no puede romperse porque el código de los módulos que lo componen está compartido.
No obstante, conforme los microservicios abandonan el plano teórico para utilizarse, cada vez más, en desarrollos reales, aparece su lado oscuro: la complejidad de la explotación. En efecto, ejecutar una constelación de microservicios requiere un clúster de contenedores que a su vez precisa de un orquestador (típicamente kubernetes). Se requieren también mecanismos de comunicación entre microservicios como colas, sistemas de suscripción y de eventos. Todo ello complica enormemente la infraestructura de ejecución, pero también el despliegue.
Donde en la época monolítica se desplegaba un simple elemento, ahora se requiere desplegar infinidad de nodos diferentes. Al final se hacen necesarios mecanismos de integración continua (CI) para automatizar dichos despliegues y evitar errores humanos. Los microservicios son un producto de la ejecución en la “nube”, pero no todos los clientes están preparados, y la infraestructura tradicional no es capaz de ejecutarlos.
Tampoco es desdeñable la complejidad de implementar mecanismos transaccionales. Una transacción, casi con seguridad, va a implicar la ejecución de varios microservicios, cada uno de ellos con su propia BBDD. Un complicado sistema de eventos debe seguir los estados por los que se transita y deben existir mecanismos de marcha atrás para los casos de error. Surgen nuevos patrones de diseño como las sagas.
Los microservicios resuelven problemas de acoplamiento, pero aumenta mucho la complejidad de operaciones que con los antiguos monolitos eran mucho más sencillas.
¿Y si simplificamos? ¿Se puede tener lo mejor de los dos mundos?
¿Por qué no tener un código modular, con interfaces bien definidos, donde cada módulo realice sólo una función, sin código compartido, sin compartir datos, pero que se despliegue en un único componente?
Un único componente, con implementación modular interna podría desplegarse en infraestructura tradicional, sin requerir un orquestador o infraestructura de microservicios.
Monolitos modulares (modulitos)
Este escenario existe. Son los monolitos modulares. Son monolitos, sí, pero en su interior está dividido en módulos, y cada módulo es tan independiente del resto que podría llegar a ejecutarse en un componente independiente.
Los monolitos modulares podrían considerarse como una agrupación de microservicios en componentes más grandes para facilitar su explotación. También podrían considerarse como un monolito preparado para ser descompuesto en servicios independientes. Puede, por tanto, pensarse como el punto de partida hacia los microservicios, o como una solución al problema de excesiva granularidad. Estos dos enfoques son los que definen, precisamente, los escenarios de su ejecución.
Escenarios de ejecución de monolitos modulares
Escenario 1: Es el primer paso en nuestro proyecto. Desarrollamos un único ejecutable pero compuesto de módulos que surgen de la descomposición del dominio, para dejar más adelante la decisión de su modelo de ejecución.
Escenario 2: Un paso de optimización de ejecución, donde, para reducir el número de ejecutables, se ha decido agrupar los microservicios en instalables más grandes.
Reglas que debe cumplir un modulito
Para definir correctamente los módulos que componen el monolito, y mantener su falta de acoplamiento, se deben seguir las siguientes reglas:
- Los módulos deben estar aislados de otros módulos. No deben compartir datos ni clases de su implementación, que es privada.
- Las clases que un módulo expone para que otro módulo pueda importarlas se denominan interfaces, y los paquetes que las contienen son los paquetes API. Las clases interfaz son para compartir estructuras de datos. No deben tener lógica de aplicación.
- No deben existir dependencias circulares. Si Modulo-B importa clases de Modulo-C, Modulo-C no puede (aunque sea de forma indirecta) llegar a importar clases de Modulo-B.
- Los datos no se comparten. Se transmiten entre módulos a través de eventos.
Si se cumplen estas reglas básicas, el monolito que construyamos estará 100% modularizado, nos permitirá un mantenimiento más fácil y, llegado el caso, podría descomponerse en servicios independientes con un esfuerzo mínimo.

Spring Modulith
Spring Modultih es la solución de Spring para garantizar la aplicación de las reglas anteriores. Es una herramienta dentro del ecosistema de Spring Boot que, en tiempo de construcción, evalúa esas reglas provocando un error de build (y por lo tanto, abortando la construcción como un error de compilación) en caso de que no se satisfagan. De esta forma, si no hay errores, podemos garantizar que el código generado está correctamente modularizado.
La construcción comienza, típicamente, realizando la descomposición de nuestro dominio en los elementos que lo componen y creando un módulo por cada elemento del dominio.

Para realizarlo con Spring Modulith, primeramente, habrá que importar la dependencia de Spring Modulith en el pom.xml de nuestro proyecto en Spring boot.

Organizaremos el código de forma que cada módulo esté contenido dentro de un paquete que cuelgue directamente del paquete de aplicación.

Dentro de cada módulo, las clases que se encuentran en sub-carpetas son privadas y no pueden importarse por otros módulos.
Las clases que se encuentran en el directorio raíz del árbol de directorios de cada módulo son, por defecto, públicas y pueden importarse por otros módulos. El directorio raíz de cada módulo es el directorio API.

Esta organización deja perfectamente definido cada módulo, con las clases que se pueden exportar (los interfaces) y su implementación privada.
Para que Spring Modulith evalúe la calidad de desacoplamiento de nuestros módulos, se añade un test que se ejecuta en la fase de construcción y que llama a la verificación de Spring Modulith con la sentencia: ApplicationModules.of(Application.class).verify();
Esta sentencia, que vemos en el ejemplo incluido, verifica la estructura de módulos que hemos definido y provoca un error de construcción en caso de que alguna regla de modularización no se cumpla.

Si las reglas se satisfacen, la construcción del paquete de Spring Boot tendrá éxito, se generará nuestro ejecutable Spring Boot, y estaremos seguros de que nuestros módulos están bien construidos.
Spring modulith también puede usarse a modo de documentación. En el mismo ejemplo anterior, el printout de módulos: “println(modules.toString()”;
muestra la estructura de módulos que hemos creado en formato texto. También es posible generar una documentación en formato UML usando la clase Documenter.

Que generaría un diagrama como el siguiente:

Conclusiones
Los modulitos modulares son una forma de generar ejecutables fáciles de desplegar en infraestructura tradicional, pero modularizados para que su mantenimiento sea sencillo. Para ello se han de cumplir ciertas reglas de diseño.
Spring Modulith surge para poder garantizar que el código que generamos con Spring Boot está correctamente modularizado y cumple las reglas de diseño, además provee información sobre los módulos y sus dependencias en formato texto y UML.
