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 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.
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.
// 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.
<!-- 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.
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.javaCette 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.
// 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;
}
}// 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.
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.
// É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()
);
}
}// 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.
// 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.
// 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
}// 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 :
-- 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.
// 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
) {}
}// 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.
// 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
internaldepuis 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.
// 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
// 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é
});
}
}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.
// 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;// 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"
}// 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.
// 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é// É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
) {}// 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.
// 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);
}
}# 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 ARCHIVEEndpoint Actuator des modules
Spring Modulith expose un endpoint Actuator pour visualiser l'état des modules en production.
// 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 :
{
"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.
// 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
Partager
Articles similaires

Spring Batch 5 en entretien technique : partitioning, chunks et fault tolerance
Préparez vos entretiens Spring Batch 5 : 15 questions essentielles sur le partitioning, chunk-oriented processing, fault tolerance avec exemples de code Java 21.

Questions entretien Spring Boot : propagation des transactions expliquée
Maîtrisez la propagation des transactions Spring Boot : REQUIRED, REQUIRES_NEW, NESTED et plus. 12 questions d'entretien avec exemples de code et pièges courants.

Spring Security 6 : Authentification JWT complète
Guide pratique pour implémenter une authentification JWT avec Spring Security 6. Configuration, génération de tokens, validation et bonnes pratiques.