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: arquitectura de monolito modular con Spring Boot

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.

AntiPattern.javajava
// 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
<!-- 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.

text
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.

Order.javajava
// 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;
    }
}
OrderRepository.javajava
// 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.

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.

OrderCreatedEvent.javajava
// 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()
        );
    }
}
OrderService.javajava
// 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.

NotificationEventListener.javajava
// 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.

EventPublicationConfig.javajava
// 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
}
InventoryEventListener.javajava
// 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:

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.

CustomerLookup.javajava
// 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
    ) {}
}
CustomerLookupImpl.javajava
// 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.

ModularityTests.javajava
// 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.

OrderModuleIntegrationTests.javajava
// 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
OrderNotificationIntegrationTest.javajava
// 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.

package-info.javajava
// 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;
NamedInterface.javajava
// 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"
}
package-info.javajava
// 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
StockReservedEvent.javajava
// 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
) {}
OrderStockListener.javajava
// 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.

ObservabilityConfig.javajava
// 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.

ModulithActuatorConfig.javajava
// 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.

ExtractionReadinessChecker.javajava
// 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

#spring modulith
#modular architecture
#spring boot
#java
#modular monolith

Compartir

Artículos relacionados