Saltar a contenido

El monolito modular (Spring Modulith)

Esta es la decisión de diseño que define al backend. Vale la pena entenderla bien, porque condiciona dónde escribís cada cosa y qué rompe el build.

El problema que resuelve

Un monolito clásico tiende al "gran bollo de barro": todo accede a todo, y con el tiempo nadie sabe qué depende de qué. Los microservicios resuelven eso con fronteras de red duras, pero a un costo alto: múltiples deploys, latencia, fallos parciales, y la complejidad de coordinar contratos entre procesos.

AutoCompraMod elige un punto intermedio: un solo proceso, pero con fronteras internas tan reales como las de un microservicio — sólo que verificadas por el compilador y los tests, no por la red. Eso es Spring Modulith.

Cómo se define un módulo

Cada módulo es un subpaquete directo de com.tipre.autocompras con un package-info.java que lo declara:

com.tipre.autocompras
├── tickets/      ← package-info.java con @ApplicationModule
├── catalogo/     ← package-info.java
├── pagos/
├── promos/
├── envases/
├── shared/       ← @ApplicationModule(type = OPEN)  (kernel)
├── security/
└── monitoring/   ← el Cockpit

La regla de oro de Spring Modulith: lo que está en la raíz del paquete del módulo es API pública; lo que está en subpaquetes es interno. Un módulo puede llamar la API pública de otro, pero no sus internals.

Módulo Rol API pública (ejemplos) Llama a
tickets Orquestador. La API que ve el POS. TicketRestController, TicketService, VoucherService catalogo, pagos, promos, envases
catalogo Artículos por EAN/sucursal + búsqueda. Cache Caffeine (~100k). ArticuloRestController, ArticuloService — (hoja)
pagos intent/pay/check/cancel. Stateless. PagoRestController, PagoService — (hoja, sale a gateways)
promos Calcula promociones del ticket. PromoController, PromoService — (hoja)
envases Vales y depósitos retornables. ValeController, ValeService, EnvaseService — (hoja)
shared Kernel OPEN: lengua común. ResponseMessage, enums de pago, NucleoImpositivoDto
monitoring Cockpit / observabilidad. Lee las APIs públicas. MetricsController, TerminalsController, … — (hoja)

Las reglas no negociables

  1. Comunicación = sólo la API pública del otro módulo. Nunca internals. Si necesitás más desacople, usá eventos de aplicación (@ApplicationModuleListener) en lugar de una llamada directa.
  2. Datos: schema-por-módulo, CERO foreign keys cruzadas. Cada módulo con DataSource propio tiene su schema. Si pagos necesita un ticket, lo pide por la API de tickets, no con un JOIN entre schemas.
  3. Los bordes externos siguen siendo red. pagos → MercadoPago es red de verdad: ahí se mantienen la reconciliación durable y el manejo de estados indeterminados. No todo es una llamada en proceso.
  4. El guardrail manda. Lo que decide si una frontera está bien no es el code review: es el test.

El guardrail: ModularityTests.verify()

Spring Modulith ofrece un test que analiza el grafo de dependencias entre módulos y falla si:

  • un módulo accede a los internals de otro,
  • aparece un ciclo de dependencias entre módulos,
  • un módulo depende de otro que no declaró.
// El guardrail, conceptualmente
ApplicationModules.of(AutoComprasApplication.class).verify();

Se corre con la suite normal:

mvn test

Si el guardrail falla, NO lo destrabás tocando el test

Un fallo de verify() significa que tu cambio cruzó una frontera que no debía o introdujo un ciclo. La respuesta correcta es revisar el diseño: ¿de verdad pagos tiene que conocer a tickets? Casi siempre la solución es invertir la dependencia (que tickets orqueste) o comunicar por evento. Cambiar el test para que pase es esconder el problema, no resolverlo.

Por qué esta forma y no otra

  • Un solo deploy. La terminal habla con un backend. Operar uno es más simple que operar cinco.
  • Refactor seguro. Como las fronteras son explícitas y verificadas, mover lógica entre módulos es un cambio acotado y el build te avisa si rompés algo.
  • Camino abierto a extraer un servicio si algún día hace falta. Si un módulo necesitara escalar aparte, ya tiene su API y su schema: extraerlo es mucho más barato que partir un bollo de barro.

Para entender cómo se llegó a esta estructura desde los cinco microservicios originales, seguí con Migración strangler.