# 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. - Published: 2026-03-23 - Updated: 2026-05-03 - Author: SharpSkill - Tags: spring modulith, modular architecture, spring boot, java, modular monolith - Reading time: 18 min --- 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. > **Idea Clave** > > 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. ```java // AntiPattern.java // 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. ```xml org.springframework.modulith spring-modulith-starter-core org.springframework.modulith spring-modulith-starter-jpa org.springframework.modulith spring-modulith-starter-test test org.springframework.modulith spring-modulith-docs test org.springframework.modulith spring-modulith-bom 1.3.0 pom import ``` ### 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.java ``` Esta 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. ```java // Order.java // 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; } } ``` ```java // OrderRepository.java // 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 { // Methods specific to the Order module List 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. > **Convención de Nombres** > > 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. ```java // OrderCreatedEvent.java // 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() ); } } ``` ```java // OrderService.java // 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. ```java // NotificationEventListener.java // 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()) ); } } ``` ### 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. ```java // EventPublicationConfig.java // 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 } ``` ```java // InventoryEventListener.java // 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 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: ```sql -- 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. ```java // CustomerLookup.java // 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 findEmailById(UUID customerId); boolean exists(UUID customerId); // Specific DTO for shared information Optional findInfoById(UUID customerId); record CustomerInfo( UUID id, String email, String fullName, String preferredLanguage ) {} } ``` ```java // CustomerLookupImpl.java // 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 findEmailById(UUID customerId) { return customerRepository.findById(customerId) .map(Customer::getEmail); } @Override public boolean exists(UUID customerId) { return customerRepository.existsById(customerId); } @Override public Optional 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. ```java // ModularityTests.java // 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 `internal` desde 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. ```java // OrderModuleIntegrationTests.java // 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 ```java // OrderNotificationIntegrationTest.java // 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 }); } } ``` > **Aislamiento de Tests** > > 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. ```java // package-info.java // 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; ``` ```java // NamedInterface.java // 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" } ``` ```java // package-info.java // 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. ```java // 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 ``` ```java // StockReservedEvent.java // 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 ) {} ``` ```java // OrderStockListener.java // 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. ```java // ObservabilityConfig.java // 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); } } ``` ```yaml # 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 ARCHIVE ``` ### Endpoint Actuator de Módulos Spring Modulith expone un endpoint Actuator para visualizar el estado de los módulos en producción. ```java // ModulithActuatorConfig.java // 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: ```json { "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. ```java // ExtractionReadinessChecker.java // 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/spring-boot/spring-modulith-modular-monolith-architecture