Spring Modulith : architecture modulaire monolithique expliquée

Découvrez Spring Modulith pour construire des monolithes modulaires en Java. Architecture, modules, événements asynchrones et tests avec exemples Spring Boot 3.

Spring Modulith : architecture modulaire monolithique avec Spring Boot

Spring Modulith représente une approche pragmatique pour structurer les applications Spring Boot en modules métier cohérents. Cette architecture positionne le monolithe modulaire entre le monolithe classique et les microservices, offrant une modularité forte sans la complexité opérationnelle des systèmes distribués.

Point clé

Spring Modulith formalise les bonnes pratiques d'architecture hexagonale et de Domain-Driven Design directement dans Spring Boot, avec vérification automatique des dépendances entre modules.

Pourquoi choisir le monolithe modulaire ?

Le problème du monolithe classique

Les monolithes traditionnels souffrent d'un couplage excessif entre composants. Au fil du temps, les dépendances croisées s'accumulent et transforment l'application en "big ball of mud" impossible à maintenir. Un changement dans le module de facturation impacte le module utilisateurs, puis le module notifications, créant des effets de bord imprévisibles.

AntiPattern.javajava
// Couplage direct entre modules - À ÉVITER
@Service
public class OrderService {

    // Dépendances directes vers d'autres modules
    // Crée un couplage fort et des cycles de dépendances
    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) {
        // Ce service connaît trop de détails d'implémentation
        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;
    }
}

Ce pattern génère des problèmes concrets : tests d'intégration fragiles, difficultés à raisonner sur l'impact d'un changement, et impossibilité de déployer ou faire évoluer un module indépendamment.

Les microservices ne sont pas toujours la solution

Les microservices résolvent le problème du couplage mais introduisent une complexité opérationnelle significative : communication réseau, cohérence éventuelle, déploiement distribué, observabilité multi-services. Pour de nombreuses équipes, cette complexité n'est pas justifiée par les bénéfices obtenus.

Le monolithe modulaire offre une alternative : une seule unité de déploiement avec des frontières de modules clairement définies et respectées. Spring Modulith automatise la vérification de ces frontières.

Premiers pas avec Spring Modulith

Configuration du projet

L'intégration de Spring Modulith dans un projet Spring Boot 3 nécessite quelques dépendances Maven. Le starter principal active la détection automatique des modules.

xml
<!-- pom.xml -->
<!-- Dépendances Spring Modulith pour Spring Boot 3.2+ -->
<dependencies>
    <!-- Core Spring Modulith -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-core</artifactId>
    </dependency>

    <!-- Support des événements asynchrones avec persistence -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-jpa</artifactId>
    </dependency>

    <!-- Tests de structure des modules -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Génération automatique de documentation -->
    <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>

Structure des modules

Spring Modulith détecte automatiquement les modules basés sur les packages directs sous le package principal de l'application. Chaque sous-package représente un module distinct avec ses propres responsabilités.

text
com.example.shop/
├── ShopApplication.java        # Point d'entrée Spring Boot
├── order/                       # Module Order
│   ├── Order.java              # Entité publique (API du module)
│   ├── OrderService.java       # Service public
│   ├── internal/               # Package interne au module
│   │   ├── OrderRepository.java
│   │   └── OrderValidator.java
│   └── OrderCreatedEvent.java  # Événement publié
├── inventory/                   # Module Inventory
│   ├── InventoryService.java
│   ├── Product.java
│   └── internal/
│       └── StockRepository.java
├── customer/                    # Module Customer
│   ├── Customer.java
│   ├── CustomerService.java
│   └── internal/
│       └── CustomerRepository.java
└── notification/                # Module Notification
    ├── NotificationService.java
    └── internal/
        └── EmailSender.java

Cette convention établit une règle fondamentale : seules les classes du package racine du module (pas dans internal/) constituent l'API publique accessible aux autres modules.

Order.javajava
// Entité publique du module Order - accessible depuis d'autres 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;

    // Référence par ID plutôt que par entité
    // Évite le couplage direct avec le module Customer
    private UUID customerId;

    private BigDecimal totalAmount;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    private LocalDateTime createdAt;

    protected Order() {
        // Constructeur JPA
    }

    public Order(UUID customerId, BigDecimal totalAmount) {
        this.id = UUID.randomUUID();
        this.customerId = customerId;
        this.totalAmount = totalAmount;
        this.status = OrderStatus.PENDING;
        this.createdAt = LocalDateTime.now();
    }

    // Getters publics - partie de l'API du module
    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; }

    // Méthodes métier encapsulées
    void confirm() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Only pending orders can be confirmed");
        }
        this.status = OrderStatus.CONFIRMED;
    }
}
OrderRepository.javajava
// Repository interne - NON accessible depuis d'autres modules
package com.example.shop.order.internal;

import com.example.shop.order.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;

// Ce repository est dans le package internal
// Spring Modulith interdit son accès depuis d'autres modules
interface OrderRepository extends JpaRepository<Order, UUID> {

    // Méthodes spécifiques au module Order
    List<Order> findByCustomerIdAndStatus(UUID customerId, OrderStatus status);
}

Le repository reste interne car l'accès aux données passe obligatoirement par le service public, garantissant l'encapsulation de la logique métier.

Convention de nommage

Le package internal n'a rien de magique pour Java. C'est une convention que Spring Modulith reconnaît et vérifie automatiquement lors des tests. Toute violation génère une erreur explicite.

Communication entre modules

Événements de domaine

La communication entre modules s'effectue via des événements de domaine plutôt que par appels directs. Ce pattern découple les modules émetteurs des modules récepteurs.

OrderCreatedEvent.javajava
// Événement publié par le module Order
package com.example.shop.order;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

// Record immuable représentant l'événement
// Contient uniquement les informations nécessaires aux consommateurs
public record OrderCreatedEvent(
    UUID orderId,
    UUID customerId,
    BigDecimal totalAmount,
    LocalDateTime createdAt
) {
    // Factory method pour créer l'événement depuis l'entité
    public static OrderCreatedEvent from(Order order) {
        return new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getTotalAmount(),
            order.getCreatedAt()
        );
    }
}
OrderService.javajava
// Service public qui publie l'événement
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) {
        // Création de la commande
        Order order = new Order(customerId, amount);
        order = orderRepository.save(order);

        // Publication de l'événement
        // Les modules intéressés réagiront de manière asynchrone
        eventPublisher.publishEvent(OrderCreatedEvent.from(order));

        return order;
    }

    public Order confirmOrder(UUID orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));

        order.confirm();

        // Événement de confirmation
        eventPublisher.publishEvent(new OrderConfirmedEvent(
            order.getId(),
            order.getCustomerId()
        ));

        return order;
    }
}

Les autres modules consomment ces événements sans connaître les détails d'implémentation du module Order.

NotificationEventListener.javajava
// Module Notification consommant les événements Order
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 garantit le traitement asynchrone
    // et la persistence de l'événement pour retry en cas d'échec
    @ApplicationModuleListener
    void onOrderCreated(OrderCreatedEvent event) {
        // Récupère l'email via une interface locale
        // Évite la dépendance directe vers le module Customer
        String email = customerLookup.getEmailByCustomerId(event.customerId());

        emailSender.send(
            email,
            "Commande reçue",
            "Votre commande #%s a été reçue.".formatted(event.orderId())
        );
    }

    @ApplicationModuleListener
    void onOrderConfirmed(OrderConfirmedEvent event) {
        String email = customerLookup.getEmailByCustomerId(event.customerId());

        emailSender.send(
            email,
            "Commande confirmée",
            "Votre commande #%s est confirmée et en cours de préparation."
                .formatted(event.orderId())
        );
    }
}

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Événements persistés et asynchrones

Spring Modulith offre une fonctionnalité puissante : la persistence des événements. Les événements sont stockés en base de données avant publication, garantissant leur traitement même en cas de crash applicatif.

EventPublicationConfig.javajava
// Configuration des événements persistés
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  // Active la persistence des événements
public class EventPublicationConfig {

    // Spring Modulith crée automatiquement les tables nécessaires
    // EVENT_PUBLICATION stocke les événements en attente
    // Les événements traités sont marqués comme complétés
}
InventoryEventListener.javajava
// Listener avec gestion transactionnelle des événements
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;
    }

    // Traitement transactionnel - si l'exception est levée,
    // l'événement sera retenté automatiquement
    @ApplicationModuleListener
    @Transactional
    void onOrderCreated(OrderCreatedEvent event) {
        // Réserve le stock pour cette commande
        // En cas d'échec, l'événement reste dans EVENT_PUBLICATION
        // et sera retraité lors du prochain cycle
        reserveStockForOrder(event.orderId(), event.items());
    }

    @ApplicationModuleListener
    @Transactional
    void onOrderCancelled(OrderCancelledEvent event) {
        // Libère le stock réservé
        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) {
        // Implémentation de la libération du stock
    }
}

La table EVENT_PUBLICATION créée automatiquement par Spring Modulith :

sql
-- Structure de la table EVENT_PUBLICATION (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 pour les requêtes de retry
CREATE INDEX idx_event_publication_incomplete
ON event_publication (completion_date)
WHERE completion_date IS NULL;

Interfaces exposées entre modules

Lorsqu'un module a besoin d'informations d'un autre module sans événement, une interface publique dans le module source permet un couplage minimal.

CustomerLookup.javajava
// Interface publique du module Customer
package com.example.shop.customer;

import java.util.Optional;
import java.util.UUID;

// Interface exposée aux autres modules
// Définit le contrat sans exposer les détails d'implémentation
public interface CustomerLookup {

    Optional<String> findEmailById(UUID customerId);

    boolean exists(UUID customerId);

    // DTO spécifique pour les informations partagées
    Optional<CustomerInfo> findInfoById(UUID customerId);

    record CustomerInfo(
        UUID id,
        String email,
        String fullName,
        String preferredLanguage
    ) {}
}
CustomerLookupImpl.javajava
// Implémentation interne
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()
            ));
    }
}

Cette approche permet au module Notification d'accéder aux informations client sans dépendre directement du repository ou de l'entité Customer.

Tests de structure des modules

Vérification automatique des dépendances

Spring Modulith fournit des outils de test pour vérifier que les règles d'architecture sont respectées. Ces tests échouent si un module accède aux classes internes d'un autre module.

ModularityTests.javajava
// Tests de vérification de l'architecture modulaire
package com.example.shop;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;

class ModularityTests {

    // Charge la structure des modules de l'application
    private final ApplicationModules modules = ApplicationModules.of(ShopApplication.class);

    @Test
    void verifyModularStructure() {
        // Vérifie que tous les modules sont correctement structurés
        // Échoue si un module accède aux packages internal d'un autre
        modules.verify();
    }

    @Test
    void printModuleOverview() {
        // Affiche la structure des modules dans la console
        // Utile pour comprendre les dépendances
        modules.forEach(System.out::println);
    }

    @Test
    void createModuleDocumentation() {
        // Génère une documentation automatique des modules
        // Inclut les diagrammes de dépendances
        new Documenter(modules)
            .writeModulesAsPlantUml()
            .writeIndividualModulesAsPlantUml();
    }

    @Test
    void detectCyclicDependencies() {
        // La méthode verify() détecte aussi les cycles
        // Module A → Module B → Module C → Module A = échec
        modules.verify();
    }
}

L'exécution de modules.verify() analyse le bytecode et détecte :

  • Les accès aux packages internal depuis d'autres modules
  • Les dépendances cycliques entre modules
  • Les violations des règles d'encapsulation

Tests d'intégration par module

Spring Modulith permet de tester chaque module en isolation, chargeant uniquement les beans nécessaires.

OrderModuleIntegrationTests.javajava
// Test d'intégration du module Order en 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  // Charge uniquement le module Order et ses dépendances
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 - vérifie que l'événement est publié
        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 - crée une commande
        Order order = orderService.createOrder(UUID.randomUUID(), BigDecimal.TEN);

        // When / Then - confirme et vérifie l'événement
        scenario.stimulate(() -> orderService.confirmOrder(order.getId()))
            .andWaitForEventOfType(OrderConfirmedEvent.class)
            .toArriveAndVerify(event -> {
                assertThat(event.orderId()).isEqualTo(order.getId());
            });
    }
}

L'annotation @ApplicationModuleTest configure automatiquement :

  • Le chargement des beans du module Order uniquement
  • Les mocks pour les dépendances vers d'autres modules
  • L'infrastructure de test pour les événements
OrderNotificationIntegrationTest.javajava
// Test d'intégration inter-modules
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 charge tous les modules directement dépendants
@ApplicationModuleTest(BootstrapMode.DIRECT)
class OrderNotificationIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Test
    void shouldTriggerNotificationOnOrderCreated(Scenario scenario) {
        // Ce test vérifie l'intégration Order → Notification
        UUID customerId = UUID.randomUUID();

        scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
            .andWaitForEventOfType(OrderCreatedEvent.class)
            .toArriveAndVerify(event -> {
                // L'événement a été traité par NotificationEventListener
                // Le test vérifie que l'email a été envoyé
            });
    }
}
Isolation des tests

Utiliser BootstrapMode.STANDALONE (défaut) pour les tests unitaires de module. Réserver BootstrapMode.ALL_DEPENDENCIES aux tests d'intégration end-to-end pour éviter les dépendances cachées.

Configuration avancée des modules

Modules explicites avec @ApplicationModule

Pour les cas complexes, l'annotation @ApplicationModule permet de configurer explicitement les règles d'un module.

package-info.javajava
// Configuration explicite du module Order
@org.springframework.modulith.ApplicationModule(
    // Modules autorisés à dépendre de celui-ci
    allowedDependencies = {"customer", "inventory"},
    // Type de module : OPEN (accès libre) ou CLOSED (API explicite)
    type = Type.CLOSED
)
package com.example.shop.order;

import org.springframework.modulith.ApplicationModule.Type;
NamedInterface.javajava
// Définition d'interfaces nommées pour une API plus fine
package com.example.shop.order;

import org.springframework.modulith.NamedInterface;

// Expose uniquement certaines classes comme API publique
@NamedInterface("order-api")
public class OrderApi {
    // Classes dans ce package sont accessibles via "order-api"
}
package-info.javajava
// Module qui dépend d'une interface nommée spécifique
@org.springframework.modulith.ApplicationModule(
    allowedDependencies = "order::order-api"  // Accès limité à l'API nommée
)
package com.example.shop.shipping;

Gestion des dépendances circulaires

Les dépendances circulaires entre modules indiquent souvent un problème de conception. Spring Modulith les détecte et échoue lors de la vérification. La solution passe généralement par l'extraction d'un nouveau module ou l'utilisation d'événements.

java
// AVANT - Dépendance circulaire
// Order → Inventory (pour vérifier le stock)
// Inventory → Order (pour connaître les commandes en cours)

// APRÈS - Résolution par événements
// Order publie OrderCreatedEvent
// Inventory écoute et réserve le stock
// Inventory publie StockReservedEvent
// Order écoute et confirme la disponibilité
StockReservedEvent.javajava
// Événement publié par 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
// Module Order écoute les événements Inventory
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);
    }
}

Observabilité et monitoring

Traçage des événements

Spring Modulith s'intègre avec Micrometer pour le traçage distribué des événements entre modules.

ObservabilityConfig.javajava
// Configuration de l'observabilité des modules
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) {
        // Ajoute des spans pour chaque événement traité
        return new ModuleEventListener(registry);
    }
}
yaml
# application.yml
# Configuration de l'observabilité
management:
  tracing:
    sampling:
      probability: 1.0  # Trace tous les événements en dev
  endpoints:
    web:
      exposure:
        include: health,info,metrics,modulith

spring:
  modulith:
    events:
      # Intervalle de retry pour les événements échoués
      republish-outstanding-events-on-restart: true
      # Durée de conservation des événements complétés
      completion-mode: DELETE  # ou ARCHIVE

Endpoint Actuator des modules

Spring Modulith expose un endpoint Actuator pour visualiser l'état des modules en production.

ModulithActuatorConfig.javajava
// Activation de l'endpoint Actuator
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);
    }
}

L'endpoint /actuator/modulith retourne :

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"
      ]
    }
  ]
}

Migration vers les microservices

Préparation de l'extraction

L'architecture modulaire facilite l'extraction future vers les microservices. Chaque module devient un candidat naturel pour l'extraction.

ExtractionReadinessChecker.javajava
// Vérification de la préparation à l'extraction
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());

        // Un module prêt pour l'extraction :
        // - Communique uniquement par événements
        // - N'a pas de dépendances synchrones vers d'autres modules
        // - Possède ses propres tables de données
    }
}

Les modules qui communiquent exclusivement par événements peuvent être extraits en microservices avec un minimum de modifications : remplacer le bus d'événements local par un broker de messages (Kafka, RabbitMQ).

Conclusion

Spring Modulith apporte une solution pragmatique pour structurer les applications Spring Boot monolithiques :

Structure conventionnelle : packages = modules, internal = encapsulation

Communication découplée : événements de domaine entre modules

Vérification automatique : tests de structure détectant les violations

Événements persistés : garantie de traitement avec @ApplicationModuleListener

Tests isolés : @ApplicationModuleTest pour tester chaque module

Documentation générée : diagrammes PlantUML automatiques

Observabilité : intégration Micrometer et endpoint Actuator

Chemin vers microservices : extraction facilitée par le découplage

Cette architecture convient particulièrement aux équipes qui souhaitent structurer leur monolithe sans la complexité opérationnelle des microservices, tout en gardant la possibilité d'évoluer vers une architecture distribuée si nécessaire.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#spring modulith
#architecture modulaire
#spring boot
#java
#monolithe modulaire

Partager

Articles similaires