Salta al contenuto principale

07 - Migrazione Step-by-Step: Da Monolith a Modular Monolith

Introduzione: La Migrazione come Processo Incrementale Migrare un monolith legacy a un modular monolith non e un progetto big-bang che richiede mesi di...

Cosa ne pensi di questo articolo?

07 - Migrazione Step-by-Step: Da Monolith a Modular Monolith
ArchitectureFoto:RDNE Stock projectPexels

In Breve

Introduzione: La Migrazione come Processo Incrementale Migrare un monolith legacy a un modular monolith non e un progetto big-bang che richiede mesi di...

Introduzione: La Migrazione come Processo Incrementale

Migrare un monolith legacy a un modular monolith non e un progetto big-bang che richiede mesi di riscrittura e un deploy rischioso. E un processo incrementale, sicuro e reversibile, che può procedere in parallelo allo sviluppo di nuove feature. Ogni passo della migrazione produce un sistema funzionante e testabile, riducendo il rischio a ogni iterazione.

In questo articolo presenteremo un playbook di migrazione in 4 fasi: dall'audit del codice all'identificazione dei confini, dalla separazione fisica dei moduli all'introduzione della comunicazione event-driven. Includeremo una timeline realistica, strategie di testing, e gli anti-pattern da evitare.

Cosa Imparerai in Questo Articolo

  • Il pattern Strangler Fig applicato alla modularizzazione interna
  • Fase 1: Code audit e identificazione dei confini con DDD
  • Fase 2: Separazione fisica in package e moduli
  • Fase 3: Estrazione delle API e definizione dei contratti interni
  • Fase 4: Migrazione alla comunicazione event-driven
  • Strategia di testing per ogni fase
  • Rollback strategy e gestione del rischio
  • Timeline realistica: 2-6 mesi a rischio controllato
  • Anti-pattern e errori comuni da evitare

Il Pattern Strangler Fig

Il Strangler Fig Pattern, originariamente concepito per migrare da monolith a microservizi, si applica efficacemente anche alla modularizzazione interna. L'idea e semplice: invece di riscrivere tutto, avvolgi gradualmente le parti del monolith legacy con moduli ben strutturati, fino a quando il codice legacy viene completamente sostituito.

Il processo funziona cosi:

  1. Identifica un'area funzionale del monolith legacy
  2. Crea un nuovo modulo con boundaries chiare che implementa la stessa funzionalità
  3. Instrada gradualmente il traffico dal codice legacy al nuovo modulo
  4. Quando il nuovo modulo e stabile, rimuovi il codice legacy
  5. Ripeti per la prossima area funzionale

Vantaggio Chiave dello Strangler Fig

Il sistema rimane sempre funzionante durante la migrazione. Non esiste un momento in cui il sistema e "mezzo migrato e non funzionante". Ogni incremento produce un sistema completo e testabile, con la possibilità di rollback immediato.

Fase 1: Code Audit e Identificazione dei Confini

La prima fase e puramente analitica: non si modifica codice, si analizza. L'obiettivo e comprendere la struttura attuale del monolith e identificare i confini naturali dei moduli.

1.1 Analisi delle Dipendenze

Usa strumenti di analisi statica per mappare le dipendenze tra classi e package. Strumenti come JDepend, ArchUnit, o Structure101 possono generare grafi delle dipendenze che rivelano cluster naturali e accoppiamenti problematici.


// ArchUnit: analizza le dipendenze esistenti
@Test
void analyzeDependencies() {
    JavaClasses classes = new ClassFileImporter()
        .importPackages("com.legacy.app");

    // Identifica i cicli di dipendenze
    SliceRule noCycles = SlicesRuleDefinition
        .slices()
        .matching("com.legacy.app.(*)..")
        .should().beFreeOfCycles();

    // Questo test probabilmente fallirà nel monolith legacy
    // Il report mostra esattamente dove sono i cicli
    try {
        noCycles.check(classes);
    } catch (AssertionError e) {
        // Analizza i cicli per identificare i confini
        System.out.println("Dependency cycles found:");
        System.out.println(e.getMessage());
    }
}
    

1.2 Event Storming con il Team

Organizza una sessione di Event Storming con sviluppatori e stakeholder di business per identificare i bounded contexts. Questa sessione produce una mappa dei domini di business che diventera la base per i confini dei moduli.

1.3 Prioritizzazione

Non tutti i moduli hanno la stessa urgenza. Prioritizza basandoti su:

  • Frequenza di cambiamento: i moduli che cambiano più spesso beneficiano prima della modularizzazione
  • Complessità: i moduli più complessi necessitano di confini chiari prima degli altri
  • Accoppiamento: inizia dai moduli con meno dipendenze esterne (più facili da estrarre)
  • Valore di business: i moduli critici per il business meritano attenzione prioritaria

Fase 2: Separazione Fisica

In questa fase, si riorganizza il codice sorgente da un'organizzazione per layer tecnico a un'organizzazione per modulo funzionale. Questa e la modifica più visibile e può essere fatta in modo completamente retrocompatibile.


// Migrazione della struttura dei package
// FASE 2.1: Crea la nuova struttura

// PRIMA (layer-based):
// com.app.controller.OrderController
// com.app.service.OrderService
// com.app.repository.OrderRepository
// com.app.model.Order

// DOPO (module-based):
// com.app.order.api.OrderModuleApi
// com.app.order.internal.OrderController
// com.app.order.internal.OrderService
// com.app.order.internal.OrderRepository
// com.app.order.internal.domain.Order

// FASE 2.2: Sposta le classi una alla volta
// Usa il refactoring "Move Class" dell'IDE
// Il compilatore segnala immediatamente le dipendenze rotte

// FASE 2.3: Verifica che il build funzioni dopo ogni spostamento
// ./gradlew build
// Se il build fallisce, correggi le dipendenze o
// rollback lo spostamento
    

Strategia di Testing per la Fase 2

La separazione fisica non deve cambiare il comportamento. La strategia di testing e:

  • Test di regressione: esegui l'intera suite di test dopo ogni spostamento
  • Test di compilazione: il compilatore e il tuo primo test, verifica che il build funzioni
  • Test end-to-end: verifica che i flussi utente principali funzionino
  • Commit frequenti: ogni spostamento di classe e un commit, per rollback granulare

Fase 3: Estrazione delle API

Una volta che il codice e organizzato per modulo, la fase 3 introduce le interfacce pubbliche (API) tra moduli. Ogni modulo espone un'interfaccia nel package api e nasconde l'implementazione nel package internal.


// Fase 3: Estrazione dell'API dal codice esistente

// PASSO 1: Identifica i metodi chiamati da altri moduli
// Cerca tutti gli usi di OrderService al di fuori del package order
// grep -r "OrderService" --include="*.java" | grep -v "order/"

// PASSO 2: Crea l'interfaccia pubblica con solo i metodi necessari
package com.app.order.api;

public interface OrderModuleApi {
    // Solo i metodi usati da altri moduli
    OrderDto createOrder(CreateOrderCommand cmd);
    Optional<OrderDto> findById(UUID id);
    List<OrderDto> findByUserId(UUID userId);
}

// PASSO 3: Implementa l'interfaccia nel servizio esistente
package com.app.order.internal;

@Service
class OrderService implements OrderModuleApi {
    // Il codice esistente non cambia
    // Aggiungi solo "implements OrderModuleApi"
    // e i metodi toDto() per le conversioni

    @Override
    public OrderDto createOrder(CreateOrderCommand cmd) {
        // Logica esistente...
        Order order = new Order(cmd);
        orderRepository.save(order);
        return OrderDto.from(order);
    }
}

// PASSO 4: Aggiorna i chiamanti per usare l'interfaccia
// Prima:  private final OrderService orderService;
// Dopo:   private final OrderModuleApi orderModule;
    

Fase 4: Comunicazione Event-Driven

L'ultima fase introduce la comunicazione basata su eventi tra moduli, sostituendo gradualmente le chiamate sincrone dirette dove il disaccoppiamento e vantaggioso. Non tutte le interazioni devono diventare event-driven: le chiamate sincrone restano valide per le query e le operazioni che richiedono consistenza forte.


// Fase 4: Da chiamata sincrona a evento

// PRIMA: accoppiamento sincrono
@Service
class OrderService {
    private final NotificationService notificationService;
    private final InventoryService inventoryService;

    public void createOrder(CreateOrderCommand cmd) {
        Order order = Order.create(cmd);
        orderRepo.save(order);
        // Chiamate sincrone accoppiate
        notificationService.sendConfirmation(order);
        inventoryService.reserveStock(order.getItems());
    }
}

// DOPO: disaccoppiamento con eventi
@Service
class OrderService implements OrderModuleApi {
    private final ApplicationEventPublisher events;

    @Transactional
    public OrderDto createOrder(CreateOrderCommand cmd) {
        Order order = Order.create(cmd);
        orderRepo.save(order);
        // Pubblica evento: i consumatori reagiscono autonomamente
        events.publishEvent(new OrderCreatedEvent(
            order.getId(), order.getUserId(), order.getItems()
        ));
        return order.toDto();
    }
}

// Il modulo Notification reagisce all'evento
@Service
class NotificationHandler {
    @TransactionalEventListener(phase = AFTER_COMMIT)
    void onOrderCreated(OrderCreatedEvent event) {
        notificationService.sendConfirmation(event.userId());
    }
}

// Il modulo Inventory reagisce allo stesso evento
@Service
class InventoryHandler {
    @TransactionalEventListener(phase = AFTER_COMMIT)
    void onOrderCreated(OrderCreatedEvent event) {
        inventoryService.reserveStock(event.items());
    }
}
    

Timeline Realistica

Ecco una timeline realistica per un monolith medio (50-100k LOC, 3-5 sviluppatori dedicati):

  • Settimana 1-2: Fase 1 - Code audit, event storming, prioritizzazione
  • Settimana 3-8: Fase 2 - Separazione fisica, un modulo alla volta
  • Settimana 9-14: Fase 3 - Estrazione API, definizione contratti
  • Settimana 15-20: Fase 4 - Event-driven communication
  • Settimana 21-24: Stabilizzazione, testing, documentazione

Totale: 4-6 mesi per una migrazione completa, con il sistema sempre funzionante e la possibilità di rilasciare feature durante la migrazione.

Rollback Strategy

Ogni fase della migrazione deve essere reversibile. Ecco la strategia di rollback per ogni fase:

  • Fase 1: Nessun codice modificato, nessun rollback necessario
  • Fase 2: Ogni spostamento di classe e un commit. Rollback = git revert
  • Fase 3: L'interfaccia e additiva. Rollback = rimuovi l'interfaccia, torna alle chiamate dirette
  • Fase 4: Gli eventi sono additivi. Rollback = rimuovi i listener, torna alle chiamate sincrone

Anti-Pattern da Evitare

Ecco gli errori più comuni durante la migrazione e come evitarli:

1. Big Bang Rewrite

Errore: riscrivere tutto da zero in un nuovo progetto, per poi fare uno switch in produzione. Soluzione: migrazione incrementale con il pattern Strangler Fig.

2. Premature Extraction

Errore: estrarre moduli come microservizi prima di avere i confini chiari. Soluzione: completa la modularizzazione interna prima di considerare l'estrazione.

3. Shared Mutable State

Errore: moduli che condividono oggetti mutabili in memoria. Soluzione: comunicare solo attraverso DTO immutabili e eventi.

4. Circular Dependencies

Errore: modulo A dipende da modulo B che dipende da modulo A. Soluzione: introduci un event bus per rompere i cicli, o crea uno shared kernel.

5. Testing Insufficiente

Errore: migrare senza una suite di test adeguata. Soluzione: prima di iniziare la migrazione, assicurati di avere test end-to-end che coprano i flussi principali. Questi test sono la tua rete di sicurezza.

Prossimo Articolo

Nel prossimo e ultimo articolo della serie, presenteremo un case study completo: una startup con 12 microservizi che ha migrato a modular monolith. Vedremo metriche before/after, ROI quantificato, costi risparmiati, e le lezioni apprese durante i 4 mesi di migrazione.

Domande frequenti

Qual è il vantaggio principale del processo di migrazione incrementale rispetto a un progetto big-bang?

Il processo di migrazione incrementale riduce il rischio e consente di procedere in parallelo allo sviluppo di nuove feature, con ogni passo che produce un sistema funzionante e testabile.

Qual è il Pattern Strangler Fig e come si applica alla modularizzazione interna?

Il Pattern Strangler Fig consiste nell'avvolgere gradualmente le parti del monolith legacy con moduli ben strutturati, fino a quando il codice legacy viene completamente sostituito.

Quali sono i passaggi della Fase 1 di migrazione e cosa si cerca di ottenere?

I passaggi della Fase 1 includono l'analisi delle dipendenze, la sessione di Event Storming con il team e la priorità dei moduli. L'obiettivo è comprendere la struttura attuale del monolith e identificare i confini naturali dei moduli.

Qual è la strategia di rollback per ogni fase della migrazione?

Ogni fase della migrazione deve essere reversibile, quindi si utilizza una strategia di rollback che consente di tornare indietro immediatamente in caso di problemi.

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