Salta al contenuto principale

02 - Modular Monolith: Architettura Ibrida tra Monolith e Microservices

Introduzione: Il Modular Monolith come Architettura Ibrida Nel precedente articolo abbiamo analizzato la crisi dei microservizi e i segnali di allarme che...

Cosa ne pensi di questo articolo?

02 - Modular Monolith: Architettura Ibrida tra Monolith e Microservices
ArchitectureFoto:Felicity TaiPexels

In Breve

Introduzione: Il Modular Monolith come Architettura Ibrida Nel precedente articolo abbiamo analizzato la crisi dei microservizi e i segnali di allarme che...

Introduzione: Il Modular Monolith come Architettura Ibrida

Nel precedente articolo abbiamo analizzato la crisi dei microservizi e i segnali di allarme che indicano quando la distribuzione diventa un problema. Ora e il momento di esplorare la soluzione: il Modular Monolith, un'architettura che combina la semplicità operativa di un singolo processo deployabile con la modularita e l'indipendenza logica tipiche dei microservizi.

Il modular monolith non e un compromesso al ribasso, ma una scelta architetturale consapevole che permette di ottenere il meglio di entrambi i mondi. In questo articolo vedremo come progettare module boundaries, definire internal APIs, e come framework come Spring Modulith possono aiutare a mantenere l'integrita architetturale nel tempo.

Cosa Imparerai in Questo Articolo

  • I principi fondamentali dell'architettura modular monolith
  • Come definire module boundaries chiare e mantenibili
  • Il ruolo delle internal APIs e dei contratti tra moduli
  • Spring Modulith: il framework che applica le boundaries a compile time
  • Confronto strutturato: monolith, modular monolith, microservizi
  • Architettura di riferimento con diagrammi e codice

Principi Fondamentali del Modular Monolith

Un modular monolith si distingue da un monolith tradizionale per quattro principi architetturali che governano la struttura del codice e le interazioni tra componenti:

1. Single Deployable, Multiple Logical Modules

L'applicazione viene compilata e deployata come un singolo artefatto (un JAR, un WAR, un binario), ma internamente e organizzata in moduli logici indipendenti. Ogni modulo ha il proprio package, le proprie classi, e una superficie API pubblica ben definita. I moduli comunicano esclusivamente attraverso interfacce pubbliche, mai accedendo a classi interne di altri moduli.

2. Encapsulation e Information Hiding

Ogni modulo nasconde la propria implementazione interna dietro un'interfaccia pubblica. Le classi di dominio, i repository, i servizi interni sono inaccessibili dall'esterno. Solo le interfacce API, i DTO e gli eventi sono esposti. Questo garantisce che i moduli possano evolvere internamente senza rompere i consumatori.

3. Data Ownership

Ogni modulo e proprietario esclusivo dei propri dati. Anche se il database fisico può essere condiviso, ogni modulo accede solo alle proprie tabelle attraverso il proprio repository. Nessun modulo può leggere o scrivere direttamente nelle tabelle di un altro modulo: deve passare attraverso l'API pubblica.

4. Comunicazione Esplicita

Le interazioni tra moduli avvengono attraverso canali espliciti: chiamate di metodo attraverso interfacce, eventi di dominio, o comandi/query. Non esistono dipendenze nascoste o accessi diretti al database di altri moduli. Ogni dipendenza e visibile e tracciabile.

La Differenza Chiave

In un monolith tradizionale, qualsiasi classe può chiamare qualsiasi altra classe. In un modular monolith, i moduli possono comunicare solo attraverso interfacce pubbliche. Questa restrizione e ciò che rende possibile l'eventuale estrazione di un modulo come microservizio.

Architettura di Riferimento

Vediamo come si struttura un modular monolith in pratica. L'esempio seguente mostra un'applicazione e-commerce con quattro moduli: Order, Catalog, User e Payment.


// Struttura del progetto modular monolith
// com.ecommerce/
// ├── order/
// │   ├── api/
// │   │   ├── OrderModuleApi.java        (interfaccia pubblica)
// │   │   ├── OrderDto.java              (DTO pubblico)
// │   │   └── OrderCreatedEvent.java     (evento pubblico)
// │   └── internal/
// │       ├── OrderService.java          (logica privata)
// │       ├── OrderRepository.java       (accesso dati privato)
// │       └── Order.java                 (entità privata)
// ├── catalog/
// │   ├── api/
// │   │   ├── CatalogModuleApi.java
// │   │   └── ProductDto.java
// │   └── internal/
// │       ├── CatalogService.java
// │       └── Product.java
// ├── user/
// │   ├── api/
// │   │   └── UserModuleApi.java
// │   └── internal/
// │       └── UserService.java
// └── payment/
//     ├── api/
//     │   └── PaymentModuleApi.java
//     └── internal/
//         └── PaymentService.java
    

Definizione delle Internal APIs

Ogni modulo espone un'interfaccia che definisce il contratto pubblico del modulo. Questa interfaccia e l'unico punto di accesso per gli altri moduli. L'implementazione e nascosta nel package internal.


// API pubblica del modulo Order
package com.ecommerce.order.api;

public interface OrderModuleApi {

    /**
     * Crea un nuovo ordine.
     * Pubblica OrderCreatedEvent al completamento.
     */
    OrderDto createOrder(CreateOrderCommand command);

    /**
     * Recupera un ordine per ID.
     * @throws OrderNotFoundException se l'ordine non esiste
     */
    Optional<OrderDto> findById(UUID orderId);

    /**
     * Lista ordini di un utente con paginazione.
     */
    Page<OrderDto> findByUserId(UUID userId, Pageable pageable);

    /**
     * Annulla un ordine esistente.
     * Pubblica OrderCancelledEvent al completamento.
     */
    void cancelOrder(UUID orderId);
}

// DTO pubblico - solo dati, nessuna logica di dominio
public record OrderDto(
    UUID id,
    UUID userId,
    List<OrderItemDto> items,
    BigDecimal total,
    OrderStatus status,
    Instant createdAt
) {}
    

Implementazione Interna del Modulo

L'implementazione del modulo e completamente nascosta. Le classi nel package internal non sono accessibili dall'esterno. Questo permette di refactorizzare liberamente l'implementazione senza impattare i consumatori.


// Implementazione privata del modulo Order
package com.ecommerce.order.internal;

@Service
class OrderServiceImpl implements OrderModuleApi {

    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;
    private final CatalogModuleApi catalogModule; // dipendenza da altro modulo

    OrderServiceImpl(OrderRepository orderRepository,
                     ApplicationEventPublisher eventPublisher,
                     CatalogModuleApi catalogModule) {
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
        this.catalogModule = catalogModule;
    }

    @Override
    @Transactional
    public OrderDto createOrder(CreateOrderCommand command) {
        // Verifica prodotti tramite API pubblica del modulo Catalog
        List<ProductDto> products = command.getItemIds().stream()
            .map(catalogModule::findById)
            .map(opt -> opt.orElseThrow(ProductNotFoundException::new))
            .toList();

        // Crea l'entità di dominio (classe privata)
        Order order = Order.create(command.getUserId(), products);
        orderRepository.save(order);

        // Pubblica evento per altri moduli interessati
        eventPublisher.publishEvent(
            new OrderCreatedEvent(order.getId(), order.getUserId())
        );

        return order.toDto();
    }
}
    

Spring Modulith: Boundaries a Compile Time

Spring Modulith e un framework ufficiale di Spring che fornisce strumenti per strutturare, verificare e documentare modular monoliths in Spring Boot. La sua caratteristica principale e la capacità di verificare le boundaries tra moduli a compile time, impedendo accessi non autorizzati tra package.

Configurazione di Spring Modulith


<!-- pom.xml - Dipendenze Spring Modulith -->
<dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-test</artifactId>
    <scope>test</scope>
</dependency>
    

Test di Verifica delle Boundaries

Spring Modulith fornisce un test automatico che verifica che le regole di accesso tra moduli siano rispettate. Se un modulo accede a classi interne di un altro modulo, il test fallisce.


// Test che verifica l'integrita delle boundaries
@Test
void verifyModularStructure() {
    ApplicationModules modules = ApplicationModules.of(
        EcommerceApplication.class
    );

    // Verifica che nessun modulo acceda a classi interne
    // di un altro modulo
    modules.verify();

    // Genera documentazione automatica dei moduli
    new Documenter(modules)
        .writeModulesAsPlantUml()
        .writeIndividualModulesAsPlantUml();
}

// Output del test in caso di violazione:
// Module 'payment' depends on non-exposed type
// com.ecommerce.order.internal.Order
// from module 'order'
    

Event-based Communication con Spring Modulith

Spring Modulith supporta nativamente la comunicazione tra moduli basata su eventi. Gli eventi vengono pubblicati tramite ApplicationEventPublisher e consumati con @ApplicationModuleListener, garantendo disaccoppiamento tra produttore e consumatore.


// Modulo Order: pubblica un evento
@Service
class OrderServiceImpl implements OrderModuleApi {

    private final ApplicationEventPublisher events;

    @Transactional
    public OrderDto createOrder(CreateOrderCommand cmd) {
        Order order = Order.create(cmd);
        orderRepository.save(order);
        events.publishEvent(new OrderCreatedEvent(
            order.getId(), order.getUserId(), order.getTotal()
        ));
        return order.toDto();
    }
}

// Modulo Payment: reagisce all'evento
@Service
class PaymentEventHandler {

    @ApplicationModuleListener
    void onOrderCreated(OrderCreatedEvent event) {
        // Processa il pagamento quando un ordine viene creato
        paymentService.processPayment(
            event.userId(), event.total()
        );
    }
}

// Modulo Notification: reagisce allo stesso evento
@Service
class NotificationEventHandler {

    @ApplicationModuleListener
    void onOrderCreated(OrderCreatedEvent event) {
        notificationService.sendOrderConfirmation(event.userId());
    }
}
    

Confronto: Monolith vs Modular Monolith vs Microservizi

Per comprendere dove si posiziona il modular monolith nel panorama architetturale, confrontiamo le tre architetture su dimensioni chiave:

Complessità Operativa

  • Monolith tradizionale: Bassa. Un deploy, un processo, log centralizzati. Ma il codice interno può diventare un groviglio
  • Modular Monolith: Bassa. Stessi vantaggi operativi del monolith, con codice interno strutturato e manutenibile
  • Microservizi: Alta. N pipeline, N deploy, distributed tracing, service mesh, network policies

Scalabilità

  • Monolith tradizionale: Orizzontale (più istanze). Sufficiente per il 95% dei casi
  • Modular Monolith: Orizzontale + possibilità di estrarre moduli critici come servizi indipendenti
  • Microservizi: Indipendente per servizio. Necessario solo per carichi molto asimmetrici

Autonomia dei Team

  • Monolith tradizionale: Limitata. Tutti lavorano sullo stesso codebase senza confini chiari
  • Modular Monolith: Moderata. Team per modulo con interfacce chiare, deploy condiviso
  • Microservizi: Alta. Team completamente indipendenti, deploy indipendenti

Costi

  • Monolith tradizionale: Minimi. Infrastruttura semplice
  • Modular Monolith: Minimi-moderati. Stesso costo infrastrutturale del monolith
  • Microservizi: Elevati. 6x rispetto al monolith secondo stime di settore

Il Sweet Spot del Modular Monolith

Il modular monolith si posiziona nel punto ottimale per la maggior parte delle organizzazioni: mantiene i costi operativi bassi del monolith, offre la modularita necessaria per team di medie dimensioni, e preserva la possibilità di estrarre microservizi in futuro quando i dati lo giustificano.

Quando Estrarre un Modulo come Microservizio

Il modular monolith non e necessariamente il punto di arrivo. E piuttosto un punto di partenza solido da cui estrarre microservizi quando necessario. Ma come sapere quando e il momento giusto?

Ecco i segnali che giustificano l'estrazione di un modulo:

  • Scaling asimmetrico: il modulo richiede 10x più risorse rispetto agli altri
  • Team dedicato: un team di 5+ persone lavora esclusivamente su quel modulo e necessità di deploy indipendente
  • Stack tecnologico diverso: il modulo beneficerebbe di un linguaggio o runtime diverso
  • Frequenza di rilascio diversa: il modulo necessità di rilasci giornalieri mentre il resto del sistema e settimanale
  • Isolation failure: un bug nel modulo può causare il crash dell'intero sistema

Implementazione Pratica: Primo Passo

Se hai un monolith esistente e vuoi iniziare la transizione verso un modular monolith, il primo passo e organizzare il codice in package per feature piuttosto che per layer tecnico. Invece di separare per controller, service, repository, separa per order, catalog, user.


// PRIMA: organizzazione per layer tecnico (sbagliato)
// com.app/
// ├── controller/
// │   ├── OrderController.java
// │   ├── UserController.java
// │   └── CatalogController.java
// ├── service/
// │   ├── OrderService.java
// │   ├── UserService.java
// │   └── CatalogService.java
// └── repository/
//     ├── OrderRepository.java
//     ├── UserRepository.java
//     └── CatalogRepository.java

// DOPO: organizzazione per feature/modulo (corretto)
// com.app/
// ├── order/
// │   ├── api/OrderModuleApi.java
// │   └── internal/
// │       ├── OrderController.java
// │       ├── OrderService.java
// │       └── OrderRepository.java
// ├── user/
// │   ├── api/UserModuleApi.java
// │   └── internal/
// │       ├── UserController.java
// │       ├── UserService.java
// │       └── UserRepository.java
// └── catalog/
//     ├── api/CatalogModuleApi.java
//     └── internal/
//         ├── CatalogController.java
//         ├── CatalogService.java
//         └── CatalogRepository.java
    

Prossimi Passi

In questo articolo abbiamo definito i principi fondamentali del modular monolith e visto come strutturare il codice con boundaries chiare. Nei prossimi articoli approfondiremo ogni aspetto:

  • Articolo 3: Come usare il Domain Driven Design per identificare i bounded contexts e definire i confini dei moduli
  • Articolo 4: Come progettare il database per un modular monolith, con pattern per schema condiviso e separato
  • Articolo 5: I pattern di comunicazione tra moduli: sincroni, asincroni ed event-driven

Prossimo Articolo

Nel prossimo articolo esploreremo il Domain Driven Design e come utilizzarlo per identificare i bounded contexts della tua applicazione. Vedremo concetti come ubiquitous language, context mapping, aggregate design, e come tradurli in confini di modulo concreti nel codice.

Domande frequenti

Qual è la principale differenza tra un monolith tradizionale e un Modular Monolith?

La principale differenza sta nei quattro principi architetturali che governano la struttura del codice e le interazioni tra componenti nel Modular Monolith, come l'encapsulation, la proprietà dei dati e la comunicazione esplicita.

Come si definiscono le internal APIs in un Modular Monolith?

Le internal APIs sono definite come l'unico punto di accesso per gli altri moduli, esponendo un'interfaccia che definisce il contratto pubblico del modulo e nascondendo l'implementazione interna.

Qual è lo scopo principale dello strumento Spring Modulith?

Lo strumento Spring Modulith fornisce strumenti per strutturare, verificare e documentare modular monoliths in Spring Boot, con la capacità di verificare le boundaries tra moduli a compile time.

Qual è il vantaggio principale del Modular Monolith rispetto ai microservizi?

Il Modular Monolith mantiene i costi operativi bassi del monolith e offre la modularità necessaria per team di medie dimensioni, preservando anche la possibilità di estrazione di microservizi in futuro.

Fonti e approfondimenti

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti