# Spring Modulith: Architettura del Monolite Modulare Spiegata
> Impara Spring Modulith per costruire monoliti modulari in Java. Architettura, moduli, eventi asincroni e testing con esempi Spring Boot 3.
- Published: 2026-03-23
- Updated: 2026-05-03
- Author: SharpSkill
- Tags: spring modulith, modular architecture, spring boot, java, modular monolith
- Reading time: 18 min
---
Spring Modulith propone un approccio pragmatico per strutturare le applicazioni Spring Boot in moduli di business coesi. Questa architettura colloca il monolite modulare tra il monolite tradizionale e i microservizi, offrendo una solida modularità senza la complessità operativa dei sistemi distribuiti.
> **Insight Chiave**
>
> Spring Modulith formalizza le best practice dell'architettura esagonale e del Domain-Driven Design direttamente in Spring Boot, con verifica automatica delle dipendenze tra moduli.
## Perché Scegliere un Monolite Modulare?
### Il Problema del Monolite Classico
I monoliti tradizionali soffrono di un accoppiamento eccessivo tra i componenti. Con il tempo le dipendenze incrociate si accumulano e trasformano l'applicazione in un "big ball of mud" impossibile da mantenere. Una modifica al modulo di fatturazione impatta il modulo utenti, poi quello di notifica, generando effetti collaterali imprevedibili.
```java
// AntiPattern.java
// Direct coupling between modules - AVOID THIS
@Service
public class OrderService {
// Direct dependencies to other modules
// Creates tight coupling and dependency cycles
private final UserRepository userRepository;
private final InventoryService inventoryService;
private final PaymentProcessor paymentProcessor;
private final NotificationService notificationService;
private final ShippingCalculator shippingCalculator;
public OrderService(UserRepository userRepository,
InventoryService inventoryService,
PaymentProcessor paymentProcessor,
NotificationService notificationService,
ShippingCalculator shippingCalculator) {
this.userRepository = userRepository;
this.inventoryService = inventoryService;
this.paymentProcessor = paymentProcessor;
this.notificationService = notificationService;
this.shippingCalculator = shippingCalculator;
}
public Order createOrder(OrderRequest request) {
// This service knows too many implementation details
User user = userRepository.findById(request.userId()).orElseThrow();
inventoryService.reserveItems(request.items());
BigDecimal shipping = shippingCalculator.calculate(user.getAddress());
paymentProcessor.charge(user, request.total().add(shipping));
notificationService.sendOrderConfirmation(user, request);
// ...
return null;
}
}
```
Questo pattern produce problemi concreti: test di integrazione fragili, difficoltà a ragionare sull'impatto di una modifica e impossibilità di rilasciare o evolvere un modulo in modo indipendente.
### I Microservizi Non Sono Sempre la Risposta
I microservizi risolvono il problema dell'accoppiamento, ma introducono una complessità operativa significativa: comunicazione di rete, eventual consistency, deploy distribuito, osservabilità multi-servizio. Per molti team questa complessità non è giustificata dai benefici ottenuti.
Il monolite modulare offre un'alternativa: una singola unità di deploy con confini di modulo chiaramente definiti e applicati. Spring Modulith automatizza la verifica di tali confini.
## Primi Passi con Spring Modulith
### Configurazione del Progetto
Integrare Spring Modulith in un progetto Spring Boot 3 richiede alcune dipendenze Maven. Lo starter principale abilita il rilevamento automatico dei moduli.
```xml
org.springframework.modulith
spring-modulith-starter-core
org.springframework.modulith
spring-modulith-starter-jpa
org.springframework.modulith
spring-modulith-starter-test
test
org.springframework.modulith
spring-modulith-docs
test
org.springframework.modulith
spring-modulith-bom
1.3.0
pom
import
```
### Struttura dei Moduli
Spring Modulith rileva automaticamente i moduli a partire dai package diretti sotto il package principale dell'applicazione. Ogni sotto-package rappresenta un modulo distinto con responsabilità proprie.
```
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
```
Questa convenzione stabilisce una regola fondamentale: solo le classi presenti nel package radice del modulo (non in `internal/`) costituiscono l'API pubblica accessibile dagli altri moduli.
```java
// Order.java
// Public entity of Order module - accessible from other modules
package com.example.shop.order;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "orders")
public class Order {
@Id
private UUID id;
// Reference by ID rather than entity
// Avoids direct coupling with Customer module
private UUID customerId;
private BigDecimal totalAmount;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private LocalDateTime createdAt;
protected Order() {
// JPA constructor
}
public Order(UUID customerId, BigDecimal totalAmount) {
this.id = UUID.randomUUID();
this.customerId = customerId;
this.totalAmount = totalAmount;
this.status = OrderStatus.PENDING;
this.createdAt = LocalDateTime.now();
}
// Public getters - part of module API
public UUID getId() { return id; }
public UUID getCustomerId() { return customerId; }
public BigDecimal getTotalAmount() { return totalAmount; }
public OrderStatus getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
// Encapsulated business methods
void confirm() {
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException("Only pending orders can be confirmed");
}
this.status = OrderStatus.CONFIRMED;
}
}
```
```java
// OrderRepository.java
// Internal repository - NOT accessible from other modules
package com.example.shop.order.internal;
import com.example.shop.order.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
// This repository is in the internal package
// Spring Modulith prohibits access from other modules
interface OrderRepository extends JpaRepository {
// Methods specific to the Order module
List findByCustomerIdAndStatus(UUID customerId, OrderStatus status);
}
```
Il repository resta interno perché l'accesso ai dati deve passare dal servizio pubblico, garantendo l'incapsulamento della logica di business.
> **Convenzione di Naming**
>
> Il package `internal` non ha nulla di magico per Java. È una convenzione che Spring Modulith riconosce e verifica automaticamente durante i test. Ogni violazione genera un errore esplicito.
## Comunicazione tra Moduli
### Eventi di Dominio
La comunicazione tra moduli avviene tramite eventi di dominio anziché chiamate dirette. Questo pattern disaccoppia i moduli emittenti dai moduli ricevitori.
```java
// OrderCreatedEvent.java
// Event published by Order module
package com.example.shop.order;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
// Immutable record representing the event
// Contains only information needed by consumers
public record OrderCreatedEvent(
UUID orderId,
UUID customerId,
BigDecimal totalAmount,
LocalDateTime createdAt
) {
// Factory method to create event from entity
public static OrderCreatedEvent from(Order order) {
return new OrderCreatedEvent(
order.getId(),
order.getCustomerId(),
order.getTotalAmount(),
order.getCreatedAt()
);
}
}
```
```java
// OrderService.java
// Public service that publishes events
package com.example.shop.order;
import com.example.shop.order.internal.OrderRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.UUID;
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepository,
ApplicationEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}
public Order createOrder(UUID customerId, BigDecimal amount) {
// Create the order
Order order = new Order(customerId, amount);
order = orderRepository.save(order);
// Publish the event
// Interested modules will react asynchronously
eventPublisher.publishEvent(OrderCreatedEvent.from(order));
return order;
}
public Order confirmOrder(UUID orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirm();
// Confirmation event
eventPublisher.publishEvent(new OrderConfirmedEvent(
order.getId(),
order.getCustomerId()
));
return order;
}
}
```
Gli altri moduli consumano questi eventi senza conoscere i dettagli implementativi del modulo Order.
```java
// NotificationEventListener.java
// Notification module consuming Order events
package com.example.shop.notification.internal;
import com.example.shop.order.OrderCreatedEvent;
import com.example.shop.order.OrderConfirmedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
@Component
class NotificationEventListener {
private final EmailSender emailSender;
private final CustomerLookup customerLookup;
NotificationEventListener(EmailSender emailSender,
CustomerLookup customerLookup) {
this.emailSender = emailSender;
this.customerLookup = customerLookup;
}
// @ApplicationModuleListener guarantees async processing
// and event persistence for retry on failure
@ApplicationModuleListener
void onOrderCreated(OrderCreatedEvent event) {
// Retrieve email via local interface
// Avoids direct dependency on Customer module
String email = customerLookup.getEmailByCustomerId(event.customerId());
emailSender.send(
email,
"Order Received",
"Your order #%s has been received.".formatted(event.orderId())
);
}
@ApplicationModuleListener
void onOrderConfirmed(OrderConfirmedEvent event) {
String email = customerLookup.getEmailByCustomerId(event.customerId());
emailSender.send(
email,
"Order Confirmed",
"Your order #%s is confirmed and being prepared."
.formatted(event.orderId())
);
}
}
```
### Eventi Persistiti e Asincroni
Spring Modulith offre una funzionalità potente: la persistenza degli eventi. Gli eventi vengono memorizzati nel database prima della pubblicazione, garantendone l'elaborazione anche in caso di crash dell'applicazione.
```java
// EventPublicationConfig.java
// Persisted events configuration
package com.example.shop.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.events.config.EnablePersistentDomainEvents;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
@EnablePersistentDomainEvents // Enables event persistence
public class EventPublicationConfig {
// Spring Modulith automatically creates required tables
// EVENT_PUBLICATION stores pending events
// Processed events are marked as completed
}
```
```java
// InventoryEventListener.java
// Listener with transactional event handling
package com.example.shop.inventory.internal;
import com.example.shop.order.OrderCreatedEvent;
import com.example.shop.order.OrderCancelledEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
class InventoryEventListener {
private final StockRepository stockRepository;
InventoryEventListener(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
// Transactional processing - if exception thrown,
// event will be retried automatically
@ApplicationModuleListener
@Transactional
void onOrderCreated(OrderCreatedEvent event) {
// Reserve stock for this order
// On failure, event remains in EVENT_PUBLICATION
// and will be reprocessed in next cycle
reserveStockForOrder(event.orderId(), event.items());
}
@ApplicationModuleListener
@Transactional
void onOrderCancelled(OrderCancelledEvent event) {
// Release reserved stock
releaseStockForOrder(event.orderId());
}
private void reserveStockForOrder(UUID orderId, List items) {
for (OrderItem item : items) {
Stock stock = stockRepository.findByProductId(item.productId())
.orElseThrow(() -> new StockNotFoundException(item.productId()));
stock.reserve(item.quantity());
stockRepository.save(stock);
}
}
private void releaseStockForOrder(UUID orderId) {
// Stock release implementation
}
}
```
La tabella `EVENT_PUBLICATION` creata automaticamente da 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;
```
### Interfacce Esposte tra Moduli
Quando un modulo necessita di informazioni da un altro senza ricorrere a un evento, un'interfaccia pubblica nel modulo sorgente permette un accoppiamento minimo.
```java
// CustomerLookup.java
// Public interface of Customer module
package com.example.shop.customer;
import java.util.Optional;
import java.util.UUID;
// Interface exposed to other modules
// Defines contract without exposing implementation details
public interface CustomerLookup {
Optional findEmailById(UUID customerId);
boolean exists(UUID customerId);
// Specific DTO for shared information
Optional findInfoById(UUID customerId);
record CustomerInfo(
UUID id,
String email,
String fullName,
String preferredLanguage
) {}
}
```
```java
// CustomerLookupImpl.java
// Internal implementation
package com.example.shop.customer.internal;
import com.example.shop.customer.CustomerLookup;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.UUID;
@Component
class CustomerLookupImpl implements CustomerLookup {
private final CustomerRepository customerRepository;
CustomerLookupImpl(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
@Override
public Optional findEmailById(UUID customerId) {
return customerRepository.findById(customerId)
.map(Customer::getEmail);
}
@Override
public boolean exists(UUID customerId) {
return customerRepository.existsById(customerId);
}
@Override
public Optional findInfoById(UUID customerId) {
return customerRepository.findById(customerId)
.map(customer -> new CustomerInfo(
customer.getId(),
customer.getEmail(),
customer.getFullName(),
customer.getPreferredLanguage()
));
}
}
```
Questo approccio consente al modulo Notification di accedere alle informazioni del cliente senza dipendere direttamente dal repository o dall'entità Customer.
## Test della Struttura Modulare
### Verifica Automatica delle Dipendenze
Spring Modulith fornisce strumenti di testing per validare il rispetto delle regole architetturali. Questi test falliscono se un modulo accede a classi interne di un altro.
```java
// ModularityTests.java
// Modular architecture verification tests
package com.example.shop;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;
class ModularityTests {
// Load application module structure
private final ApplicationModules modules = ApplicationModules.of(ShopApplication.class);
@Test
void verifyModularStructure() {
// Verify all modules are correctly structured
// Fails if a module accesses internal packages of another
modules.verify();
}
@Test
void printModuleOverview() {
// Print module structure to console
// Useful for understanding dependencies
modules.forEach(System.out::println);
}
@Test
void createModuleDocumentation() {
// Generate automatic module documentation
// Includes dependency diagrams
new Documenter(modules)
.writeModulesAsPlantUml()
.writeIndividualModulesAsPlantUml();
}
@Test
void detectCyclicDependencies() {
// The verify() method also detects cycles
// Module A → Module B → Module C → Module A = failure
modules.verify();
}
}
```
L'esecuzione di `modules.verify()` analizza il bytecode e rileva:
- Accessi ai package `internal` da altri moduli
- Dipendenze cicliche tra moduli
- Violazioni delle regole di incapsulamento
### Test di Integrazione per Modulo
Spring Modulith permette di testare ogni modulo in isolamento, caricando solo i bean necessari.
```java
// OrderModuleIntegrationTests.java
// Order module integration test in isolation
package com.example.shop.order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import java.math.BigDecimal;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@ApplicationModuleTest // Load only Order module and its dependencies
class OrderModuleIntegrationTests {
@Autowired
private OrderService orderService;
@Test
void shouldCreateOrder() {
// Given
UUID customerId = UUID.randomUUID();
BigDecimal amount = new BigDecimal("99.99");
// When
Order order = orderService.createOrder(customerId, amount);
// Then
assertThat(order.getId()).isNotNull();
assertThat(order.getCustomerId()).isEqualTo(customerId);
assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
}
@Test
void shouldPublishEventOnOrderCreation(Scenario scenario) {
// Given
UUID customerId = UUID.randomUUID();
// When / Then - verify event is published
scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
.andWaitForEventOfType(OrderCreatedEvent.class)
.matching(event -> event.customerId().equals(customerId))
.toArriveAndVerify(event -> {
assertThat(event.orderId()).isNotNull();
assertThat(event.totalAmount()).isEqualTo(BigDecimal.TEN);
});
}
@Test
void shouldHandleOrderConfirmation(Scenario scenario) {
// Given - create an order
Order order = orderService.createOrder(UUID.randomUUID(), BigDecimal.TEN);
// When / Then - confirm and verify event
scenario.stimulate(() -> orderService.confirmOrder(order.getId()))
.andWaitForEventOfType(OrderConfirmedEvent.class)
.toArriveAndVerify(event -> {
assertThat(event.orderId()).isEqualTo(order.getId());
});
}
}
```
L'annotazione `@ApplicationModuleTest` configura automaticamente:
- Il caricamento dei soli bean del modulo Order
- Mock per le dipendenze verso altri moduli
- L'infrastruttura di test degli eventi
```java
// OrderNotificationIntegrationTest.java
// Inter-module integration test
package com.example.shop;
import com.example.shop.order.OrderService;
import com.example.shop.order.OrderCreatedEvent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode;
import java.math.BigDecimal;
import java.util.UUID;
// DIRECT loads all directly dependent modules
@ApplicationModuleTest(BootstrapMode.DIRECT)
class OrderNotificationIntegrationTest {
@Autowired
private OrderService orderService;
@Test
void shouldTriggerNotificationOnOrderCreated(Scenario scenario) {
// This test verifies Order → Notification integration
UUID customerId = UUID.randomUUID();
scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
.andWaitForEventOfType(OrderCreatedEvent.class)
.toArriveAndVerify(event -> {
// Event was processed by NotificationEventListener
// Test verifies email was sent
});
}
}
```
> **Isolamento dei Test**
>
> Usa `BootstrapMode.STANDALONE` (default) per i test unitari di modulo. Riserva `BootstrapMode.ALL_DEPENDENCIES` ai test di integrazione end-to-end, evitando dipendenze nascoste.
## Configurazione Avanzata dei Moduli
### Moduli Espliciti con @ApplicationModule
Per casi complessi, l'annotazione `@ApplicationModule` permette di configurare in modo esplicito le regole di un modulo.
```java
// package-info.java
// Explicit Order module configuration
@org.springframework.modulith.ApplicationModule(
// Modules allowed to depend on this one
allowedDependencies = {"customer", "inventory"},
// Module type: OPEN (free access) or CLOSED (explicit API)
type = Type.CLOSED
)
package com.example.shop.order;
import org.springframework.modulith.ApplicationModule.Type;
```
```java
// NamedInterface.java
// Named interface definition for finer API control
package com.example.shop.order;
import org.springframework.modulith.NamedInterface;
// Exposes only certain classes as public API
@NamedInterface("order-api")
public class OrderApi {
// Classes in this package are accessible via "order-api"
}
```
```java
// package-info.java
// Module depending on a specific named interface
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order::order-api" // Access limited to named API
)
package com.example.shop.shipping;
```
### Gestione delle Dipendenze Circolari
Le dipendenze circolari tra moduli indicano spesso un problema di design. Spring Modulith le rileva e fa fallire la verifica. La soluzione tipica è estrarre un nuovo modulo o utilizzare gli eventi.
```java
// BEFORE - Circular dependency
// Order → Inventory (to check stock)
// Inventory → Order (to know current orders)
// AFTER - Resolution through events
// Order publishes OrderCreatedEvent
// Inventory listens and reserves stock
// Inventory publishes StockReservedEvent
// Order listens and confirms availability
```
```java
// StockReservedEvent.java
// Event published by Inventory
package com.example.shop.inventory;
import java.util.UUID;
public record StockReservedEvent(
UUID orderId,
UUID productId,
int quantity,
boolean success,
String failureReason
) {}
```
```java
// OrderStockListener.java
// Order module listens to Inventory events
package com.example.shop.order.internal;
import com.example.shop.inventory.StockReservedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
@Component
class OrderStockListener {
private final OrderRepository orderRepository;
OrderStockListener(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@ApplicationModuleListener
void onStockReserved(StockReservedEvent event) {
Order order = orderRepository.findById(event.orderId())
.orElseThrow();
if (event.success()) {
order.markStockReserved();
} else {
order.markStockUnavailable(event.failureReason());
}
orderRepository.save(order);
}
}
```
## Osservabilità e Monitoraggio
### Tracing degli Eventi
Spring Modulith si integra con Micrometer per il tracing distribuito degli eventi tra moduli.
```java
// ObservabilityConfig.java
// Module observability configuration
package com.example.shop.config;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.observability.ModuleEventListener;
@Configuration
public class ObservabilityConfig {
@Bean
ModuleEventListener moduleEventListener(ObservationRegistry registry) {
// Adds spans for each processed event
return new ModuleEventListener(registry);
}
}
```
```yaml
# application.yml
# Observability configuration
management:
tracing:
sampling:
probability: 1.0 # Trace all events in dev
endpoints:
web:
exposure:
include: health,info,metrics,modulith
spring:
modulith:
events:
# Retry interval for failed events
republish-outstanding-events-on-restart: true
# Retention duration for completed events
completion-mode: DELETE # or ARCHIVE
```
### Endpoint Actuator dei Moduli
Spring Modulith espone un endpoint Actuator per visualizzare lo stato dei moduli in produzione.
```java
// ModulithActuatorConfig.java
// Actuator endpoint activation
package com.example.shop.config;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.actuator.ApplicationModulesEndpoint;
import org.springframework.modulith.core.ApplicationModules;
@Configuration
public class ModulithActuatorConfig {
@Bean
@ConditionalOnAvailableEndpoint
ApplicationModulesEndpoint modulesEndpoint(ApplicationModules modules) {
return new ApplicationModulesEndpoint(modules);
}
}
```
L'endpoint `/actuator/modulith` restituisce:
```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"
]
}
]
}
```
## Migrazione verso i Microservizi
### Preparare l'Estrazione
L'architettura modulare facilita una futura estrazione verso microservizi. Ogni modulo diventa un candidato naturale all'estrazione.
```java
// ExtractionReadinessChecker.java
// Extraction readiness verification
package com.example.shop;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.ApplicationModule;
public class ExtractionReadinessChecker {
public void checkModule(String moduleName) {
ApplicationModules modules = ApplicationModules.of(ShopApplication.class);
ApplicationModule module = modules.getModuleByName(moduleName)
.orElseThrow();
System.out.println("Module: " + moduleName);
System.out.println("Dependencies: " + module.getDependencies());
System.out.println("Published Events: " + module.getPublishedEvents());
System.out.println("Listened Events: " + module.getBootstrapDependencies());
// A module ready for extraction:
// - Communicates only through events
// - Has no synchronous dependencies to other modules
// - Owns its own data tables
}
}
```
I moduli che comunicano esclusivamente tramite eventi possono essere estratti come microservizi con modifiche minime: basta sostituire il bus eventi locale con un message broker (Kafka, RabbitMQ).
## Conclusione
Spring Modulith fornisce una soluzione pragmatica per strutturare applicazioni Spring Boot monolitiche:
✅ **Struttura per convenzione**: package = moduli, `internal` = incapsulamento
✅ **Comunicazione disaccoppiata**: eventi di dominio tra moduli
✅ **Verifica automatica**: test di struttura che rilevano le violazioni
✅ **Eventi persistiti**: garanzia di elaborazione con `@ApplicationModuleListener`
✅ **Test isolati**: `@ApplicationModuleTest` per validare ogni modulo
✅ **Documentazione generata**: diagrammi PlantUML automatici
✅ **Osservabilità**: integrazione Micrometer ed endpoint Actuator
✅ **Percorso verso i microservizi**: estrazione facilitata dal disaccoppiamento
Questa architettura si adatta in particolare ai team che vogliono strutturare il proprio monolite senza la complessità operativa dei microservizi, mantenendo l'opzione di evolvere verso un'architettura distribuita quando necessario.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/it/blog/spring-boot/spring-modulith-modular-monolith-architecture