El problema que vamos a resolver
Un desarrollador necesita generar una factura desde pedidos y reutiliza una clase interna de facturación. El código compila, pero pedidos queda acoplado a un detalle que facturación debería poder cambiar. Vamos a provocar ese error y hacer que una prueba lo detecte antes del despliegue.
El ejemplo es deliberadamente pequeño: no incluye HTTP, base de datos ni una factura real. Los UUID representan identificadores de demostración. El objetivo es comprobar dependencias entre paquetes en una aplicación Spring Boot existente.
1. Añade las dependencias
Parte de un proyecto generado con Spring Initializr que incluya Spring Modulith y soporte de tests. Conserva el BOM de Modulith y la versión compatible con tu Spring Boot que genere Initializr. Con las versiones gestionadas por ese BOM, estas son las dependencias Maven relevantes; el fragmento va dentro de <dependencies>.
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>2. Organiza dos módulos por negocio
Coloca la clase principal en io.reactor.shop. Bajo esa raíz, orders y billing serán los módulos. Por convención, los tipos públicos del paquete raíz de cada módulo forman su API; los subpaquetes, como billing.internal, quedan dentro de sus límites.
Todos los archivos de esta tabla están bajo src/main/java/io/reactor/shop/.
| Archivo | Responsabilidad |
|---|---|
ShopApplication.java | Raíz de la aplicación |
orders/OrderService.java | Módulo de pedidos |
orders/package-info.java | Dependencias permitidas |
billing/BillingService.java | API pública de facturación |
billing/internal/InvoiceNumberGenerator.java | Detalle interno de facturación |
package io.reactor.shop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}3. Expón la operación que otros módulos necesitan
Pedidos necesita solicitar una factura. Exponemos esa operación con BillingService, en el paquete público de facturación. La implementación de ejemplo devuelve un identificador; en una aplicación real, aquí delegaríamos la creación y persistencia.
package io.reactor.shop.billing;
import java.util.UUID;
import org.springframework.stereotype.Service;
@Service
public class BillingService {
public UUID issueInvoice(UUID orderId) {
// Demo: replace with invoice creation and persistence.
return UUID.randomUUID();
}
}Añadimos además una clase interna para reproducir la tentación de reutilizar un detalle de implementación. En este ejemplo no es necesario conectarla al servicio de facturación para observar la infracción.
package io.reactor.shop.billing.internal;
import java.util.UUID;
import org.springframework.stereotype.Component;
@Component
public class InvoiceNumberGenerator {
public String next() {
return "INV-" + UUID.randomUUID();
}
}4. Provoca una dependencia que Java permite
Crea primero esta versión incorrecta de OrderService. La clase interna es public, por lo que Java permite importarla. Sin embargo, queremos que ese acceso esté prohibido por la arquitectura.
package io.reactor.shop.orders;
import io.reactor.shop.billing.internal.InvoiceNumberGenerator;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final InvoiceNumberGenerator numbers;
public OrderService(InvoiceNumberGenerator numbers) {
this.numbers = numbers;
}
public String placeOrder() {
return numbers.next();
}
}5. Convierte el límite en un test
Crea src/test/java/io/reactor/shop/ModularityTest.java. Esta comprobación analiza las dependencias del código sin arrancar el contexto de Spring.
package io.reactor.shop;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
class ModularityTest {
@Test
void respectsModuleBoundaries() {
ApplicationModules.of(ShopApplication.class).verify();
}
}./mvnw -Dtest=ModularityTest testEn Windows utiliza mvnw.cmd -Dtest=ModularityTest test. Con la versión incorrecta, verify() debe fallar por el acceso desde orders al paquete interno de billing. No dependas del texto exacto del error: identifica el módulo de origen y el tipo interno referenciado.
6. Corrige el código usando la API del módulo
Sustituye el contenido de OrderService.java por esta versión. Mantén una sola definición de la clase. Pedidos solicita una factura y deja los detalles de numeración dentro de facturación.
package io.reactor.shop.orders;
import java.util.UUID;
import io.reactor.shop.billing.BillingService;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final BillingService billing;
public OrderService(BillingService billing) {
this.billing = billing;
}
public UUID placeOrder() {
UUID orderId = UUID.randomUUID();
billing.issueInvoice(orderId);
return orderId;
}
}Ejecuta de nuevo el mismo comando. En este ejemplo acotado, la comprobación debería pasar: la dependencia apunta a la API de billing. Así puedes cambiar su numeración sin modificar pedidos.
7. Declara qué dependencias aceptas
Para que pedidos solo pueda depender del módulo de facturación, añade este package-info.java. El identificador billing corresponde al nombre del módulo de este ejemplo. Una dependencia futura desde pedidos hacia otro módulo hará fallar la verificación, aunque use su API pública.
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "billing"
)
package io.reactor.shop.orders;Qué has ganado con este ejemplo
La regla ya no vive solo en una revisión de código: queda expresada en un test que puedes ejecutar en cada cambio. Spring Modulith también verifica ciclos entre módulos. Incluye este test en tu pipeline y comprueba el antes y después introduciendo temporalmente el import incorrecto.
Esta protección se aplica al análisis estático de dependencias; no convierte los paquetes en un aislamiento de seguridad en ejecución. El ejemplo tampoco resuelve transacciones, consistencia o entrega de eventos. Esas decisiones llegarán cuando el caso de negocio las necesite.
