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¶
- 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. - Datos: schema-por-módulo, CERO foreign keys cruzadas. Cada módulo con DataSource propio tiene su schema. Si
pagosnecesita un ticket, lo pide por la API detickets, no con un JOIN entre schemas. - Los bordes externos siguen siendo red.
pagos → MercadoPagoes red de verdad: ahí se mantienen la reconciliación durable y el manejo de estados indeterminados. No todo es una llamada en proceso. - 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ó.
Se corre con la suite normal:
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.