Spring Modulith: Arquitectura de Monolito Modular Explicada
Aprende Spring Modulith para construir monolitos modulares en Java. Arquitectura, módulos, eventos asíncronos y testing con ejemplos en Spring Boot 3.

Spring Modulith propone un enfoque pragmático para estructurar aplicaciones Spring Boot en módulos de negocio cohesionados. Esta arquitectura sitúa al monolito modular entre el monolito tradicional y los microservicios, ofreciendo una modularidad sólida sin la complejidad operativa de los sistemas distribuidos.
Spring Modulith formaliza las buenas prácticas de la arquitectura hexagonal y del Domain-Driven Design directamente dentro de Spring Boot, con verificación automática de las dependencias entre módulos.
¿Por Qué Optar por un Monolito Modular?
El Problema del Monolito Clásico
Los monolitos tradicionales sufren un acoplamiento excesivo entre componentes. Con el tiempo, las dependencias cruzadas se acumulan y convierten la aplicación en una "big ball of mud" imposible de mantener. Un cambio en el módulo de facturación impacta al módulo de usuarios, luego al de notificaciones, generando efectos colaterales impredecibles.
// Direct coupling between modules - AVOID THIS
@Service
public class OrderService {
// Direct dependencies to other modules
// Creates tight coupling and dependency cycles
private final UserRepository userRepository;
private final InventoryService inventoryService;
private final PaymentProcessor paymentProcessor;
private final NotificationService notificationService;
private final ShippingCalculator shippingCalculator;
public OrderService(UserRepository userRepository,
InventoryService inventoryService,
PaymentProcessor paymentProcessor,
NotificationService notificationService,
ShippingCalculator shippingCalculator) {
this.userRepository = userRepository;
this.inventoryService = inventoryService;
this.paymentProcessor = paymentProcessor;
this.notificationService = notificationService;
this.shippingCalculator = shippingCalculator;
}
public Order createOrder(OrderRequest request) {
// This service knows too many implementation details
User user = userRepository.findById(request.userId()).orElseThrow();
inventoryService.reserveItems(request.items());
BigDecimal shipping = shippingCalculator.calculate(user.getAddress());
paymentProcessor.charge(user, request.total().add(shipping));
notificationService.sendOrderConfirmation(user, request);
// ...
return null;
}
}Este patrón genera problemas concretos: tests de integración frágiles, dificultad para razonar sobre el impacto de un cambio e imposibilidad de desplegar o evolucionar un módulo de forma independiente.
Los Microservicios No Son Siempre la Respuesta
Los microservicios resuelven el problema de acoplamiento, pero introducen una complejidad operativa considerable: comunicación de red, consistencia eventual, despliegue distribuido, observabilidad multi-servicio. Para muchos equipos, esa complejidad no se justifica con los beneficios obtenidos.
El monolito modular ofrece una alternativa: una unidad de despliegue única con fronteras de módulo claramente definidas y aplicadas. Spring Modulith automatiza la verificación de esas fronteras.
Primeros Pasos con Spring Modulith
Configuración del Proyecto
Integrar Spring Modulith en un proyecto Spring Boot 3 requiere algunas dependencias Maven. El starter principal habilita la detección automática de módulos.
<!-- pom.xml -->
<!-- Spring Modulith dependencies for Spring Boot 3.2+ -->
<dependencies>
<!-- Core Spring Modulith -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<!-- Async event support with persistence -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-jpa</artifactId>
</dependency>
<!-- Module structure tests -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Automatic documentation generation -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-docs</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>1.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Estructura de Módulos
Spring Modulith detecta automáticamente los módulos a partir de los paquetes directos bajo el paquete principal de la aplicación. Cada subpaquete representa un módulo distinto con sus propias responsabilidades.
com.example.shop/
├── ShopApplication.java # Spring Boot entry point
├── order/ # Order Module
│ ├── Order.java # Public entity (module API)
│ ├── OrderService.java # Public service
│ ├── internal/ # Module-internal package
│ │ ├── OrderRepository.java
│ │ └── OrderValidator.java
│ └── OrderCreatedEvent.java # Published event
├── inventory/ # Inventory Module
│ ├── InventoryService.java
│ ├── Product.java
│ └── internal/
│ └── StockRepository.java
├── customer/ # Customer Module
│ ├── Customer.java
│ ├── CustomerService.java
│ └── internal/
│ └── CustomerRepository.java
└── notification/ # Notification Module
├── NotificationService.java
└── internal/
└── EmailSender.javaEsta convención establece una regla fundamental: solo las clases situadas en el paquete raíz del módulo (no en internal/) constituyen la API pública accesible desde otros módulos.
// Public entity of Order module - accessible from other modules
package com.example.shop.order;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "orders")
public class Order {
@Id
private UUID id;
// Reference by ID rather than entity
// Avoids direct coupling with Customer module
private UUID customerId;
private BigDecimal totalAmount;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private LocalDateTime createdAt;
protected Order() {
// JPA constructor
}
public Order(UUID customerId, BigDecimal totalAmount) {
this.id = UUID.randomUUID();
this.customerId = customerId;
this.totalAmount = totalAmount;
this.status = OrderStatus.PENDING;
this.createdAt = LocalDateTime.now();
}
// Public getters - part of module API
public UUID getId() { return id; }
public UUID getCustomerId() { return customerId; }
public BigDecimal getTotalAmount() { return totalAmount; }
public OrderStatus getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
// Encapsulated business methods
void confirm() {
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException("Only pending orders can be confirmed");
}
this.status = OrderStatus.CONFIRMED;
}
}// Internal repository - NOT accessible from other modules
package com.example.shop.order.internal;
import com.example.shop.order.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
// This repository is in the internal package
// Spring Modulith prohibits access from other modules
interface OrderRepository extends JpaRepository<Order, UUID> {
// Methods specific to the Order module
List<Order> findByCustomerIdAndStatus(UUID customerId, OrderStatus status);
}El repositorio permanece interno porque el acceso a los datos debe pasar por el servicio público, garantizando la encapsulación de la lógica de negocio.
El paquete internal no tiene nada mágico para Java. Es una convención que Spring Modulith reconoce y verifica automáticamente durante los tests. Toda violación genera un error explícito.
Comunicación Entre Módulos
Eventos de Dominio
La comunicación entre módulos pasa por eventos de dominio en lugar de llamadas directas. Este patrón desacopla los módulos emisores de los receptores.
// Event published by Order module
package com.example.shop.order;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
// Immutable record representing the event
// Contains only information needed by consumers
public record OrderCreatedEvent(
UUID orderId,
UUID customerId,
BigDecimal totalAmount,
LocalDateTime createdAt
) {
// Factory method to create event from entity
public static OrderCreatedEvent from(Order order) {
return new OrderCreatedEvent(
order.getId(),
order.getCustomerId(),
order.getTotalAmount(),
order.getCreatedAt()
);
}
}// Public service that publishes events
package com.example.shop.order;
import com.example.shop.order.internal.OrderRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.UUID;
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepository,
ApplicationEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}
public Order createOrder(UUID customerId, BigDecimal amount) {
// Create the order
Order order = new Order(customerId, amount);
order = orderRepository.save(order);
// Publish the event
// Interested modules will react asynchronously
eventPublisher.publishEvent(OrderCreatedEvent.from(order));
return order;
}
public Order confirmOrder(UUID orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirm();
// Confirmation event
eventPublisher.publishEvent(new OrderConfirmedEvent(
order.getId(),
order.getCustomerId()
));
return order;
}
}Los demás módulos consumen esos eventos sin conocer los detalles de implementación del módulo Order.
// Notification module consuming Order events
package com.example.shop.notification.internal;
import com.example.shop.order.OrderCreatedEvent;
import com.example.shop.order.OrderConfirmedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
@Component
class NotificationEventListener {
private final EmailSender emailSender;
private final CustomerLookup customerLookup;
NotificationEventListener(EmailSender emailSender,
CustomerLookup customerLookup) {
this.emailSender = emailSender;
this.customerLookup = customerLookup;
}
// @ApplicationModuleListener guarantees async processing
// and event persistence for retry on failure
@ApplicationModuleListener
void onOrderCreated(OrderCreatedEvent event) {
// Retrieve email via local interface
// Avoids direct dependency on Customer module
String email = customerLookup.getEmailByCustomerId(event.customerId());
emailSender.send(
email,
"Order Received",
"Your order #%s has been received.".formatted(event.orderId())
);
}
@ApplicationModuleListener
void onOrderConfirmed(OrderConfirmedEvent event) {
String email = customerLookup.getEmailByCustomerId(event.customerId());
emailSender.send(
email,
"Order Confirmed",
"Your order #%s is confirmed and being prepared."
.formatted(event.orderId())
);
}
}¿Listo para aprobar tus entrevistas de Spring Boot?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Eventos Persistidos y Asíncronos
Spring Modulith ofrece una funcionalidad potente: la persistencia de eventos. Los eventos se almacenan en la base de datos antes de su publicación, garantizando su procesamiento incluso si la aplicación falla.
// Persisted events configuration
package com.example.shop.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.events.config.EnablePersistentDomainEvents;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
@EnablePersistentDomainEvents // Enables event persistence
public class EventPublicationConfig {
// Spring Modulith automatically creates required tables
// EVENT_PUBLICATION stores pending events
// Processed events are marked as completed
}// Listener with transactional event handling
package com.example.shop.inventory.internal;
import com.example.shop.order.OrderCreatedEvent;
import com.example.shop.order.OrderCancelledEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
class InventoryEventListener {
private final StockRepository stockRepository;
InventoryEventListener(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
// Transactional processing - if exception thrown,
// event will be retried automatically
@ApplicationModuleListener
@Transactional
void onOrderCreated(OrderCreatedEvent event) {
// Reserve stock for this order
// On failure, event remains in EVENT_PUBLICATION
// and will be reprocessed in next cycle
reserveStockForOrder(event.orderId(), event.items());
}
@ApplicationModuleListener
@Transactional
void onOrderCancelled(OrderCancelledEvent event) {
// Release reserved stock
releaseStockForOrder(event.orderId());
}
private void reserveStockForOrder(UUID orderId, List<OrderItem> items) {
for (OrderItem item : items) {
Stock stock = stockRepository.findByProductId(item.productId())
.orElseThrow(() -> new StockNotFoundException(item.productId()));
stock.reserve(item.quantity());
stockRepository.save(stock);
}
}
private void releaseStockForOrder(UUID orderId) {
// Stock release implementation
}
}La tabla EVENT_PUBLICATION creada automáticamente por Spring Modulith:
-- EVENT_PUBLICATION table structure (PostgreSQL)
CREATE TABLE event_publication (
id UUID PRIMARY KEY,
listener_id VARCHAR(512) NOT NULL,
event_type VARCHAR(512) NOT NULL,
serialized_event TEXT NOT NULL,
publication_date TIMESTAMP NOT NULL,
completion_date TIMESTAMP
);
-- Index for retry queries
CREATE INDEX idx_event_publication_incomplete
ON event_publication (completion_date)
WHERE completion_date IS NULL;Interfaces Expuestas Entre Módulos
Cuando un módulo necesita información de otro sin recurrir a un evento, una interfaz pública en el módulo origen permite mantener un acoplamiento mínimo.
// Public interface of Customer module
package com.example.shop.customer;
import java.util.Optional;
import java.util.UUID;
// Interface exposed to other modules
// Defines contract without exposing implementation details
public interface CustomerLookup {
Optional<String> findEmailById(UUID customerId);
boolean exists(UUID customerId);
// Specific DTO for shared information
Optional<CustomerInfo> findInfoById(UUID customerId);
record CustomerInfo(
UUID id,
String email,
String fullName,
String preferredLanguage
) {}
}// Internal implementation
package com.example.shop.customer.internal;
import com.example.shop.customer.CustomerLookup;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.UUID;
@Component
class CustomerLookupImpl implements CustomerLookup {
private final CustomerRepository customerRepository;
CustomerLookupImpl(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
@Override
public Optional<String> findEmailById(UUID customerId) {
return customerRepository.findById(customerId)
.map(Customer::getEmail);
}
@Override
public boolean exists(UUID customerId) {
return customerRepository.existsById(customerId);
}
@Override
public Optional<CustomerInfo> findInfoById(UUID customerId) {
return customerRepository.findById(customerId)
.map(customer -> new CustomerInfo(
customer.getId(),
customer.getEmail(),
customer.getFullName(),
customer.getPreferredLanguage()
));
}
}Este enfoque permite al módulo Notification acceder a la información del cliente sin depender directamente del repositorio o de la entidad Customer.
Tests de Estructura Modular
Verificación Automática de Dependencias
Spring Modulith proporciona herramientas de testing para validar que se respetan las reglas arquitectónicas. Esos tests fallan si un módulo accede a las clases internas de otro.
// Modular architecture verification tests
package com.example.shop;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;
class ModularityTests {
// Load application module structure
private final ApplicationModules modules = ApplicationModules.of(ShopApplication.class);
@Test
void verifyModularStructure() {
// Verify all modules are correctly structured
// Fails if a module accesses internal packages of another
modules.verify();
}
@Test
void printModuleOverview() {
// Print module structure to console
// Useful for understanding dependencies
modules.forEach(System.out::println);
}
@Test
void createModuleDocumentation() {
// Generate automatic module documentation
// Includes dependency diagrams
new Documenter(modules)
.writeModulesAsPlantUml()
.writeIndividualModulesAsPlantUml();
}
@Test
void detectCyclicDependencies() {
// The verify() method also detects cycles
// Module A → Module B → Module C → Module A = failure
modules.verify();
}
}La ejecución de modules.verify() analiza el bytecode y detecta:
- Accesos a paquetes
internaldesde otros módulos - Dependencias cíclicas entre módulos
- Violaciones de las reglas de encapsulación
Tests de Integración por Módulo
Spring Modulith permite testear cada módulo de forma aislada, cargando únicamente los beans necesarios.
// Order module integration test in isolation
package com.example.shop.order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import java.math.BigDecimal;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@ApplicationModuleTest // Load only Order module and its dependencies
class OrderModuleIntegrationTests {
@Autowired
private OrderService orderService;
@Test
void shouldCreateOrder() {
// Given
UUID customerId = UUID.randomUUID();
BigDecimal amount = new BigDecimal("99.99");
// When
Order order = orderService.createOrder(customerId, amount);
// Then
assertThat(order.getId()).isNotNull();
assertThat(order.getCustomerId()).isEqualTo(customerId);
assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
}
@Test
void shouldPublishEventOnOrderCreation(Scenario scenario) {
// Given
UUID customerId = UUID.randomUUID();
// When / Then - verify event is published
scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
.andWaitForEventOfType(OrderCreatedEvent.class)
.matching(event -> event.customerId().equals(customerId))
.toArriveAndVerify(event -> {
assertThat(event.orderId()).isNotNull();
assertThat(event.totalAmount()).isEqualTo(BigDecimal.TEN);
});
}
@Test
void shouldHandleOrderConfirmation(Scenario scenario) {
// Given - create an order
Order order = orderService.createOrder(UUID.randomUUID(), BigDecimal.TEN);
// When / Then - confirm and verify event
scenario.stimulate(() -> orderService.confirmOrder(order.getId()))
.andWaitForEventOfType(OrderConfirmedEvent.class)
.toArriveAndVerify(event -> {
assertThat(event.orderId()).isEqualTo(order.getId());
});
}
}La anotación @ApplicationModuleTest configura automáticamente:
- La carga únicamente de los beans del módulo Order
- Mocks para las dependencias hacia otros módulos
- La infraestructura de testing de eventos
// Inter-module integration test
package com.example.shop;
import com.example.shop.order.OrderService;
import com.example.shop.order.OrderCreatedEvent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode;
import java.math.BigDecimal;
import java.util.UUID;
// DIRECT loads all directly dependent modules
@ApplicationModuleTest(BootstrapMode.DIRECT)
class OrderNotificationIntegrationTest {
@Autowired
private OrderService orderService;
@Test
void shouldTriggerNotificationOnOrderCreated(Scenario scenario) {
// This test verifies Order → Notification integration
UUID customerId = UUID.randomUUID();
scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
.andWaitForEventOfType(OrderCreatedEvent.class)
.toArriveAndVerify(event -> {
// Event was processed by NotificationEventListener
// Test verifies email was sent
});
}
}Utiliza BootstrapMode.STANDALONE (por defecto) para los tests unitarios de módulo. Reserva BootstrapMode.ALL_DEPENDENCIES para los tests de integración de extremo a extremo, evitando así dependencias ocultas.
Configuración Avanzada de Módulos
Módulos Explícitos con @ApplicationModule
Para casos complejos, la anotación @ApplicationModule permite configurar de forma explícita las reglas de un módulo.
// Explicit Order module configuration
@org.springframework.modulith.ApplicationModule(
// Modules allowed to depend on this one
allowedDependencies = {"customer", "inventory"},
// Module type: OPEN (free access) or CLOSED (explicit API)
type = Type.CLOSED
)
package com.example.shop.order;
import org.springframework.modulith.ApplicationModule.Type;// Named interface definition for finer API control
package com.example.shop.order;
import org.springframework.modulith.NamedInterface;
// Exposes only certain classes as public API
@NamedInterface("order-api")
public class OrderApi {
// Classes in this package are accessible via "order-api"
}// Module depending on a specific named interface
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order::order-api" // Access limited to named API
)
package com.example.shop.shipping;Gestión de Dependencias Circulares
Las dependencias circulares entre módulos suelen indicar un problema de diseño. Spring Modulith las detecta y falla durante la verificación. La solución pasa habitualmente por extraer un nuevo módulo o por usar eventos.
// BEFORE - Circular dependency
// Order → Inventory (to check stock)
// Inventory → Order (to know current orders)
// AFTER - Resolution through events
// Order publishes OrderCreatedEvent
// Inventory listens and reserves stock
// Inventory publishes StockReservedEvent
// Order listens and confirms availability// Event published by Inventory
package com.example.shop.inventory;
import java.util.UUID;
public record StockReservedEvent(
UUID orderId,
UUID productId,
int quantity,
boolean success,
String failureReason
) {}// Order module listens to Inventory events
package com.example.shop.order.internal;
import com.example.shop.inventory.StockReservedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
@Component
class OrderStockListener {
private final OrderRepository orderRepository;
OrderStockListener(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@ApplicationModuleListener
void onStockReserved(StockReservedEvent event) {
Order order = orderRepository.findById(event.orderId())
.orElseThrow();
if (event.success()) {
order.markStockReserved();
} else {
order.markStockUnavailable(event.failureReason());
}
orderRepository.save(order);
}
}Observabilidad y Monitorización
Tracing de Eventos
Spring Modulith se integra con Micrometer para el tracing distribuido de eventos entre módulos.
// Module observability configuration
package com.example.shop.config;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.observability.ModuleEventListener;
@Configuration
public class ObservabilityConfig {
@Bean
ModuleEventListener moduleEventListener(ObservationRegistry registry) {
// Adds spans for each processed event
return new ModuleEventListener(registry);
}
}# application.yml
# Observability configuration
management:
tracing:
sampling:
probability: 1.0 # Trace all events in dev
endpoints:
web:
exposure:
include: health,info,metrics,modulith
spring:
modulith:
events:
# Retry interval for failed events
republish-outstanding-events-on-restart: true
# Retention duration for completed events
completion-mode: DELETE # or ARCHIVEEndpoint Actuator de Módulos
Spring Modulith expone un endpoint Actuator para visualizar el estado de los módulos en producción.
// Actuator endpoint activation
package com.example.shop.config;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.actuator.ApplicationModulesEndpoint;
import org.springframework.modulith.core.ApplicationModules;
@Configuration
public class ModulithActuatorConfig {
@Bean
@ConditionalOnAvailableEndpoint
ApplicationModulesEndpoint modulesEndpoint(ApplicationModules modules) {
return new ApplicationModulesEndpoint(modules);
}
}El endpoint /actuator/modulith devuelve:
{
"modules": [
{
"name": "order",
"basePackage": "com.example.shop.order",
"dependencies": ["customer"],
"publishedEvents": [
"com.example.shop.order.OrderCreatedEvent",
"com.example.shop.order.OrderConfirmedEvent"
],
"listenedEvents": [
"com.example.shop.inventory.StockReservedEvent"
]
},
{
"name": "inventory",
"basePackage": "com.example.shop.inventory",
"dependencies": [],
"publishedEvents": [
"com.example.shop.inventory.StockReservedEvent"
],
"listenedEvents": [
"com.example.shop.order.OrderCreatedEvent"
]
}
]
}Migración a Microservicios
Preparar la Extracción
La arquitectura modular facilita una futura extracción hacia microservicios. Cada módulo se convierte en un candidato natural a la extracción.
// Extraction readiness verification
package com.example.shop;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.ApplicationModule;
public class ExtractionReadinessChecker {
public void checkModule(String moduleName) {
ApplicationModules modules = ApplicationModules.of(ShopApplication.class);
ApplicationModule module = modules.getModuleByName(moduleName)
.orElseThrow();
System.out.println("Module: " + moduleName);
System.out.println("Dependencies: " + module.getDependencies());
System.out.println("Published Events: " + module.getPublishedEvents());
System.out.println("Listened Events: " + module.getBootstrapDependencies());
// A module ready for extraction:
// - Communicates only through events
// - Has no synchronous dependencies to other modules
// - Owns its own data tables
}
}Los módulos que se comunican exclusivamente mediante eventos pueden extraerse a microservicios con cambios mínimos: basta con sustituir el bus de eventos local por un broker de mensajería (Kafka, RabbitMQ).
Conclusión
Spring Modulith aporta una solución pragmática para estructurar aplicaciones Spring Boot monolíticas:
✅ Estructura por convención: paquetes = módulos, internal = encapsulación
✅ Comunicación desacoplada: eventos de dominio entre módulos
✅ Verificación automática: tests de estructura que detectan violaciones
✅ Eventos persistidos: garantía de procesamiento con @ApplicationModuleListener
✅ Tests aislados: @ApplicationModuleTest para validar cada módulo
✅ Documentación generada: diagramas PlantUML automáticos
✅ Observabilidad: integración Micrometer y endpoint Actuator
✅ Camino hacia microservicios: extracción facilitada por el desacoplamiento
Esta arquitectura encaja especialmente con equipos que desean estructurar su monolito sin asumir la complejidad operativa de los microservicios, conservando la opción de evolucionar hacia una arquitectura distribuida cuando lo necesiten.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

Entrevista Spring Batch 5: Particionamiento, Chunks y Tolerancia
Domina las entrevistas de Spring Batch 5: 15 preguntas esenciales sobre particionamiento, procesamiento por chunks y tolerancia a fallos con ejemplos en Java 21.

Entrevista Spring Boot: Propagación de Transacciones
Domina la propagación de transacciones en Spring Boot: REQUIRED, REQUIRES_NEW, NESTED y más. 12 preguntas de entrevista con código y trampas comunes.

Spring Security 6: Autenticación JWT Completa
Guía práctica para implementar autenticación JWT con Spring Security 6. Configuración, generación de tokens, validación y buenas prácticas de seguridad.