Parte 2 — Boundaries y Reglas Modulares¶
Duración: 25 minutos
Objetivo: Entender y demostrar todas las reglas que Spring Modulith puede verificar
¿Qué hacemos aquí?¶
La Parte 1 nos enseñó a resolver violaciones. Esta parte las provoca a propósito para entender el modelo mental detrás de cada regla.
- Demostrar cómo se ve una violación de boundary en tiempo real
- Provocar y resolver una dependencia circular
- Entender cuándo y cómo usar Named Interfaces
- Ver cómo
allowedDependenciesprotege los boundaries en equipos grandes
El objetivo no es solo aprender a arreglar — es entender por qué existen estas reglas.
Los tres tipos de violaciones¶
Spring Modulith detecta tres categorías de problemas:
| Tipo | Descripción | Ejemplo |
|---|---|---|
| Tipo no expuesto | Un módulo accede a un tipo interno de otro | orders → catalog.command.ProductRepository |
| Dependencia circular | A depende de B y B depende de A | catalog → orders → catalog |
| Dependencia no declarada | Un módulo usa otro que no está en allowedDependencies |
orders → inventory cuando solo se declaró catalog, common |
Vamos a provocar y resolver cada uno.
Experimento 1: Violación de boundary en acción¶
Paso 1: Introducir una violación intencionalmente¶
Modifica temporalmente OrderService para que acceda a ProductCommandService directamente en lugar de CatalogApi:
// orders/domain/OrderService.java — MODIFICACIÓN TEMPORAL SOLO PARA EL EXPERIMENTO
import com.geovannycode.bookstore.catalog.command.ProductCommandService; // ← INCORRECTO
@Service
public class OrderService {
private final ProductCommandService commandService; // ← acceso a interno de catalog
...
}
Paso 2: Ejecutar ModularityTest¶
Resultado:
org.springframework.modulith.core.Violations:
- Module 'orders' depends on non-exposed type
com.geovannycode.bookstore.catalog.command.ProductCommandService
within module 'catalog'!
com.geovannycode.bookstore.orders.domain.OrderService
→
com.geovannycode.bookstore.catalog.command.ProductCommandService
Cómo leer el mensaje
Spring Modulith no solo dice qué está mal — muestra la cadena de dependencia exacta: qué clase en qué módulo accede a qué clase en qué otro módulo. Puedes ir directamente al código problemático sin buscar.
Paso 3: Revertir¶
Soporte en IntelliJ IDEA Ultimate
Con el plugin de Spring instalado, la violación aparece directamente en el editor antes de ejecutar el test — la línea de import se subraya en rojo con el mensaje de Spring Modulith.
Experimento 2: Dependencia circular¶
Las dependencias circulares son uno de los problemas más difíciles de detectar manualmente. Spring Modulith los encuentra en milisegundos.
Paso 1: Crear una circular intencionalmente¶
Agrega temporalmente en CatalogApi una referencia a algo del módulo orders:
// catalog/CatalogApi.java — MODIFICACIÓN TEMPORAL
import com.geovannycode.bookstore.orders.OrderCreatedEvent; // ← crea la circular
@Service
public class CatalogApi {
public void triggerCircular() {
// Esto hace que catalog → orders, pero orders ya depende de catalog.
// Resultado: catalog → orders → catalog
OrderCreatedEvent event = new OrderCreatedEvent(
null, null, null, null, 0, null, null // 7 parámetros del record
);
}
}
La cadena de dependencias queda así:
Ambos módulos se dependen mutuamente. Eso es una circular.
Paso 2: Ejecutar ModularityTest¶
Resultado:
org.springframework.modulith.core.Violations:
- Cycle detected:
Slice catalog →
Slice orders →
Slice catalog
El mensaje muestra el ciclo completo. En un proyecto con 15 módulos, detectar esto manualmente puede llevar horas. Spring Modulith lo hace en una línea.
Paso 3: ¿Por qué las circulares son peligrosas?¶
Imagina que catalog necesita publicar un CatalogUpdatedEvent que orders escucha, y orders necesita consultar catalog para validar productos. Parece razonable, pero el resultado es:
Deadlock de inicialización. La solución es que uno de los dos módulos escuche un evento en lugar de hacer una llamada directa. Nunca llames hacia atrás en el grafo de dependencias.
Paso 4: Revertir¶
Regla: Named Interfaces¶
¿Cuándo los necesitas?¶
Solo las clases en la raíz del módulo son visibles para otros. Hay casos donde quieres exponer algo de un sub-paquete sin moverlo a la raíz.
Ejemplo: supón que creces y quieres organizar los eventos en su propio sub-paquete:
orders/
├── OrderCreatedEvent.java ← en la raíz, es público ✅
└── domain/
└── events/
└── OrderShippedEvent.java ← en sub-paquete, es privado ❌
Si inventory necesita escuchar OrderShippedEvent, tienes dos opciones: moverlo a la raíz del módulo o usar @NamedInterface.
Implementando un Named Interface¶
// orders/domain/events/OrderShippedEvent.java
package com.geovannycode.bookstore.orders.domain.events;
public record OrderShippedEvent(String orderNumber, String trackingCode) {}
Para exponerlo sin moverlo a la raíz, crea un package-info.java en ese sub-paquete:
// orders/domain/events/package-info.java
/**
* Named Interface "order-events": expone los eventos de orders
* para que otros módulos puedan suscribirse.
*/
@NamedInterface("order-events")
package com.geovannycode.bookstore.orders.domain.events;
import org.springframework.modulith.NamedInterface;
Ahora inventory puede importar OrderShippedEvent y Spring Modulith lo permite sin reportarlo como violación.
En nuestra Bookstore no lo necesitamos
OrderCreatedEvent está en la raíz de orders/ directamente — es parte del contrato público del módulo. Los Named Interfaces son más útiles cuando tienes muchos eventos y quieres organizarlos en un sub-paquete dedicado sin exponerlos todos desde la raíz.
Regla: Dependencias explícitas con allowedDependencies¶
El problema¶
En la Parte 1 declaramos las dependencias de orders como {"catalog", "common"}. Pero ¿qué pasaría si no hubiéramos declarado nada?
Por defecto, cualquier módulo puede depender de cualquier otro siempre que use solo tipos públicos. Eso significa que sin allowedDependencies, alguien podría agregar mañana InventoryService a OrderService y el test no fallaría — mientras InventoryService sea público.
allowedDependencies cierra esa puerta: declara el contrato arquitectónico por escrito en el código.
Cómo lo tenemos declarado¶
En el orders/package-info.java que creamos en la Parte 1:
// orders/package-info.java
@ApplicationModule(allowedDependencies = {"catalog", "common"})
package com.geovannycode.bookstore.orders;
import org.springframework.modulith.ApplicationModule;
catalog:OrderServiceconsulta productos a través deCatalogApicommon:OrderRestControllerusaPagedResult
Cualquier dependencia fuera de esas dos genera una violación.
Demostración: intentar acceder a inventory desde orders¶
Modifica temporalmente OrderService:
// orders/domain/OrderService.java — SOLO PARA DEMOSTRACIÓN
import com.geovannycode.bookstore.inventory.InventoryService; // ← prohibido
@Service
public class OrderService {
private final InventoryService inventoryService; // ← no está en allowedDependencies
}
Ejecuta:
Resultado:
org.springframework.modulith.core.Violations:
- Module 'orders' depends on module 'inventory' via
com.geovannycode.bookstore.orders.domain.OrderService
→
com.geovannycode.bookstore.inventory.InventoryService.
Allowed targets: catalog, common.
El mensaje incluye "Allowed targets: catalog, common" — le recuerda al desarrollador exactamente cuál es el contrato declarado para ese módulo.
Revertir¶
El mapa de dependencias de la Bookstore¶
Después de esta parte, el grafo de módulos queda así:
┌─────────┐ usa API pública ┌─────────────┐
│ orders │ ─────────────────────▶│ catalog │
│ │ └─────────────┘
│ │ publica evento
│ │─────────────────────▶ ┌─────────────┐
└─────────┘ │ inventory │
└─────────────┘
┌──────────────────────────────────────────────────┐
│ common (OPEN) │
│ PagedResult, utilidades │
└──────────────────────────────────────────────────┘
Lo que garantiza este diseño:
catalogno depende de nadie — puede evolucionar librementeinventoryno llama a nadie — solo reacciona a eventosorderssolo habla concatalogvia API pública- Para extraer
inventorycomo microservicio en el futuro no necesitas tocarorders
Resumen de todas las reglas¶
| Regla | Cómo se activa | Qué previene |
|---|---|---|
| Solo API pública accesible | Por defecto | Acceso a implementaciones internas |
| Módulo OPEN | @ApplicationModule(type = OPEN) |
Sub-paquetes inaccesibles |
| Named Interface | @NamedInterface en package-info.java del sub-paquete |
Sub-paquetes específicos inaccesibles |
| Sin circulares | Por defecto | A → B → A |
| Dependencias explícitas | allowedDependencies = {...} |
Dependencias no declaradas |
Todas se verifican con un solo test — ModularityTest. Sin herramientas externas.
Checklist de la Parte 2¶
- [ ] Experimento 1: violación de boundary provocada, vista y revertida
- [ ] Experimento 2: dependencia circular provocada, vista y revertida
- [ ] Named Interfaces entendidos — cuándo usarlos vs mover a la raíz
- [ ] Demostración de
allowedDependenciescon inventory ejecutada y revertida - [ ]
ModularityTestpasa ✅
Anterior: Parte 1 — Migración y Setup Siguiente: Parte 3 — CQRS en el Módulo Catalog