DDD microservizi architetture evolutive Event Driven
Code

DDD, microservizi e architetture evolutive: perché Event-Driven

4 Giugno 2025 - 10 minuti di lettura

Nell’ultimo episodio di questa serie dedicata al Domain-Driven Design ho presentato alcuni tra i più importanti pattern tattici e spiegato il perché non si debbano condividere i Domain Event.

In questo blogpost, che prende spunto da un altro mio articolo già pubblicato nella rivista web MokaByte, vi parlerò dell’importanza delle architetture di tipo Event-Driven.

Un po’ di storia

Abbiamo già enunciato le leggi che governano le architetture software. La seconda afferma:

Why is more important than how

È quindi utile comprendere perché adottare un approccio Event-Driven rappresenti una scelta strategica.

All’inizio degli anni Settanta, Alan Kay – uno dei padri della programmazione a oggetti – insieme ad altri ricercatori dello Xerox PARC, diede vita a Smalltalk, linguaggio orientato agli oggetti.

Molti conoscono questa storia. Tuttavia, spesso si dimenticano i principi alla base della visione di Kay sull’OOP:

  • ogni elemento è un oggetto autonomo;
  • gli oggetti interagiscono inviando e ricevendo messaggi;
  • ogni oggetto racchiude dati e comportamenti;

È interessante notare come, indipendentemente da questo percorso, Carl Hewitt arrivò a conclusioni simili sviluppando l’Actor Model. Nel suo caso, il concetto di “attore” sostituiva quello di oggetto, pur senza alcun collegamento diretto con le ricerche di Kay.

Spingendosi ancora più indietro nel tempo, emergono affinità sorprendenti con un’opera filosofica pubblicata nel 1922. Ecco alcuni passaggi:

  • il mondo è tutto ciò che si verifica;
  • il mondo è la totalità dei fatti, non delle cose;
  • il mondo è determinato dai fatti e dal loro essere tutti i fatti;
  • il mondo si divide in fatti;
  • qualcosa può verificarsi o non verificarsi, e tutto il resto rimane invariato.

Se state immaginando un precursore dell’informatica moderna, vi sbagliate. Si tratta del Tractatus Logico-Philosophicus, opera di Ludwig Wittgenstein.

Tutti questi riferimenti storici e concettuali ci riportano alla domanda fondamentale: perché progettare sistemi Event-Driven? Le prime risposte arrivano proprio da qui.

Lo scenario attuale

Osservando il panorama attuale, è evidente come i sistemi distribuiti stiano trasformando profondamente il nostro approccio allo sviluppo software. In particolare, il cloud computing ha rivoluzionato il modo in cui consumiamo e condividiamo informazioni, sia tra applicazioni che tra persone.

Oggi nessun business può prescindere da una app. I dati non devono solo fluire all’interno dei nostri sistemi: devono anche poter essere scambiati con l’esterno. Solo così una soluzione può definirsi davvero competitiva. I dati devono essere accessibili, o meglio, consumabili da altri sistemi, naturalmente con i necessari livelli di sicurezza. Questa apertura ha ridefinito il modo di progettare software, affermando in modo definitivo la programmazione asincrona e relegando quella sincrona a pochi casi d’uso locali, ormai sempre più rari.

In un contesto moderno, è fondamentale poter accedere alle informazioni nel momento in cui servono, non solo quando vengono prodotte. Per questo motivo, ogni messaggio – sia che venga generato, sia che venga ricevuto tramite subscribe – deve essere persistente. In questo modo, l’informazione resta disponibile fino al momento in cui verrà effettivamente utilizzata.

È un modello che riflette esattamente il modo in cui comunichiamo nel mondo reale, come già anticipato dai lavori di Alan Kay e Carl Hewitt.

Questo cambio di paradigma non ha solo impattato le architetture software, ma ha trasformato anche le relazioni tra individui e tra aziende. Stiamo forse dicendo che Conway aveva ragione? Sì, è proprio così.

Che cosa sono i microservizi Event-Driven

I microservizi e le architetture a microservizi esistono da tempo, sebbene con nomi e approcci diversi. Chi ha più esperienza ricorderà le Service-Oriented Architectures (SOA), dove la comunicazione tra servizi era sincrona e diretta.

La comunicazione Event-Driven non è una novità in sé. Tuttavia, le esigenze attuali sono cambiate. Termini come big data, real-time e scalabilità rendono la sincronia inefficiente. Non possiamo più aspettarci che tutti i sistemi siano costantemente sincronizzati. Le false assunzioni sui sistemi distribuiti lo dimostrano chiaramente.

In un’architettura a microservizi basata su eventi, gli eventi non vengono eliminati dopo il primo consumo. Vengono invece persistiti, affinché altri servizi possano accedervi anche in un secondo momento. Questo approccio è cruciale: riflette l’evoluzione delle architetture distribuite e l’emergere di nuove tecnologie. Come affermava Fred Brooks nel suo trattato “No Silver Bullet – Essence And Accident in Software Engineering“, alcune complessità accidentali sono ormai diventate essenziali.

Il primo cambiamento è di tipo tecnologico: è ciò su cui ci concentriamo come tecnici. Ma è il “perché” che guida le scelte architetturali. Le architetture Event-Driven introducono un nuovo paradigma: i microservizi sono costruiti attorno alle esigenze del business. Ogni microservizio si occupa di una specifica funzione, un’idea lontana dalla visione originaria di SOA, fatta eccezione per pochi pionieri.

Un aspetto altrettanto rilevante è la resilienza. Non possiamo più accettare che un’intera applicazione sia inutilizzabile se una sua parte è momentaneamente offline. Tolleriamo che certe funzionalità non siano disponibili, ma l’intero sistema deve restare attivo. Questo è possibile solo se la comunicazione tra componenti avviene in modo asincrono.

Domain-Driven Design, Bounded Context e microservizi

Sembra quasi l’inizio di un film, tipo “I Tre Moschettieri”. In realtà, il Domain-Driven Design ha raggiunto la sua massima popolarità con l’avvento delle architetture a microservizi. Ho già sottolineato che non esiste una corrispondenza uno a uno tra Bounded Context e microservizi. Tuttavia, è indubbio che questi due pattern condividano molti aspetti. Il pattern del DDD è tra i più utilizzati per isolare i problemi di business da implementare in ciascun microservizio.

Immaginiamo un’azienda di qualsiasi settore: avrà sicuramente un reparto commerciale, uno vendite e uno di supporto clienti, tra gli altri. Questi reparti vengono spesso associati, durante il processo di Context Mapping, ai rispettivi Bounded Context del sistema che gestisce l’azienda. Questa suddivisione in sottodomini può proseguire ulteriormente, creando ambiti più granulari. Ciò consente di isolare meglio i vari problemi di business e di organizzare team indipendenti per gestirli. Ogni team svilupperà microservizi autonomi, che potranno evolvere in modo indipendente in base alle esigenze dei singoli contesti.

Tuttavia, possiamo davvero affermare che i nostri microservizi siano completamente indipendenti tra loro?

Coupling vs Cohesion

Quando suddividiamo il dominio della nostra applicazione in diversi sottodomini, la sfida principale è individuare e gestire le dipendenze tra di essi. È importante ricordare che suddividere un dominio non significa creare più applicazioni isolated tra loro.
Questo processo avviene “dietro le quinte” per gli sviluppatori, ma l’utente finale si aspetta di utilizzare una sola applicazione, non molteplici. In ogni caso, questi microservizi devono necessariamente comunicare tra loro, come abbiamo visto in precedenza.

Microservizi indipendenti

Per essere realmente indipendente, un microservizio deve disporre del proprio database, o di più database nel caso si separino quelli per le operazioni di lettura da quelli per la persistenza degli eventi, come nel pattern CQRS-ES. Deve inoltre avere accesso a un sistema di trasporto dei messaggi per comunicare con l’esterno e, idealmente, gestire una parte della UI a lui dedicata.

Solo garantendo questo livello di isolamento si può definire l’architettura come un’autentica architettura a microservizi. In caso contrario, si tratta di un “monolite distribuito”, che può rappresentare una fase intermedia di evoluzione, ma non l’obiettivo finale.

Quindi, tutti i microservizi sono disaccoppiati tra loro? Non esattamente. All’interno di un singolo microservizio, ogni oggetto può dipendere da un altro componente interno. Questa situazione è accettabile perché il microservizio rappresenta un componente unico che evolve nel suo insieme. I test interni garantiscono che le modifiche per introdurre nuove feature non compromettano il suo corretto funzionamento.

Il discorso cambia quando si analizzano le relazioni con gli altri microservizi.

Comunicazione asincrona

Riflettendo su quanto detto riguardo a SOA e alla comunicazione sincrona tra i servizi, oggi è chiaro perché, in un moderno sistema distribuito, questo tipo di comunicazione non sia più accettabile. Se due microservizi comunicassero in modo sincrono, si creerebbe una dipendenza tra loro. Di conseguenza, ogni modifica a uno dei due implicherebbe quasi sempre la necessità di aggiornare anche l’altro. Questo impedirebbe rilasci indipendenti.
Ricordate quando abbiamo parlato della necessità di separare gli eventi consumati all’interno di un Bounded Context (Domain Event) da quelli condivisi con altri Bounded Context (Integration Event)? Ora sostituiamo Bounded Context con microservizio e il concetto diventa ancora più chiaro.

Se l’evento che un microservizio emette verso l’esterno differisce da quello che consuma internamente, è possibile modificare — o versionare — l’evento interno senza dover informare chi consuma quello esterno. Quest’ultimo infatti rimane invariato. Così si elimina la dipendenza tra microservizi.

Ricordate il Principio di Robustezza, noto anche come legge di Postel? Dobbiamo essere rigidi nel modificare i contratti esterni, perché rappresentano una dipendenza verso altri sistemi. Cambiarli implica informare i consumatori, riscrivere i contract test e riallineare l’intero sistema. In pratica, si perde l’indipendenza nel rilascio.

E se invece si devono modificare anche gli eventi di integrazione? Nessun problema. Una caratteristica delle Architetture Evolutive è proprio quella di supportare più versioni dello stesso servizio. Possiamo quindi emettere due integration event: uno per chi richiede la nuova versione, e un altro per chi continua a usare quella precedente.

Conway’s Law e strutture di comunicazione

La cosiddetta legge di Conway afferma:

Organizations which design systems […] are constrained to produce designs which are copies of the communication structures of these organizations.

Questa celebre citazione di Melvin Conway, risalente al 1968, viene spesso richiamata nel contesto dei sistemi distribuiti. Pur essendo stata formulata in un’epoca in cui architetture di questo tipo erano impensabili, resta ancora oggi estremamente attuale.
Nell’esempio precedente sulla divisione in sottodomini, ho descritto alcune aree di business all’interno di un’azienda generica. Quando i team si organizzano attorno a queste aree, tendono a sviluppare servizi che riflettono i confini del loro business. In altre parole, condividono informazioni specifiche del loro contesto, con vantaggi e svantaggi.

Si potrebbero aprire interi capitoli sul tema di quali informazioni condividere e quali invece mantenere confidenziali, ma questo esula dal nostro scopo.

Informazioni condivise e livelli di indipendenza

Le informazioni che ogni team condivide con gli altri determinano il livello di indipendenza che ciascun team può costruire. Proviamo a vedere un esempio pratico.
Se il team responsabile del sottodominio magazzino non condivide le giacenze dei prodotti, gli altri team non potranno replicare questi dati nei propri database. Questo significa che dovranno sempre dipendere dal microservizio del magazzino ogni volta che serve conoscere la disponibilità di un prodotto.

Se, per esempio, il team vendite deve implementare una funzionalità sugli ordini di vendita e deve chiedere al team — cioè al microservizio — del magazzino se un prodotto è disponibile, significa che tra i due microservizi esiste una dipendenza.

Questa dipendenza può essere gestita in modi diversi, non necessariamente con comunicazione sincrona, ma dovrà essere comunque risolta per poter fornire al cliente una data di consegna prevista.

Inverse Conway Maneuver

Un’interessante esplorazione di questo problema è stata fatta da Martin Fowler, che tratta il problema e propone una soluzione interessante denominata Inverse Conway Maneuver.

La “Inverse Conway Maneuver” di Martin Fowler.
La “Inverse Conway Maneuver” di Martin Fowler.

Informazioni condivise e livelli di indipendenza

Le informazioni che ogni team condivide con gli altri determinano il livello di indipendenza che ciascun team può costruire. Vediamo un esempio pratico.
Se il team responsabile del sottodominio magazzino non condivide le giacenze dei prodotti, gli altri team non potranno replicare questi dati nei propri database. Di conseguenza, dovranno sempre dipendere dal microservizio del magazzino ogni volta che serve conoscere la disponibilità di un prodotto.

Se il team vendite, per implementare una funzionalità sugli ordini di vendita, deve chiedere al team — ovvero al microservizio — del magazzino se un prodotto è disponibile, significa che tra i due microservizi esiste una dipendenza.

Questa dipendenza può essere gestita in modi diversi, non necessariamente con comunicazione sincrona, ma deve comunque essere risolta per fornire al cliente una data di consegna attendibile.

Esplorare nuove modalità all’interno dell’azienda

Cambiare i pattern di comunicazione in un’azienda non è mai semplice. Significa modificare equilibri delicati costruiti nel tempo e adottare un nuovo approccio nella gestione dei problemi. In questo processo, alcune figure possono percepire una diminuzione del loro potere all’interno dell’organizzazione.
Se fino a ieri, per confermare una data di consegna, dovevi chiedere a me, responsabile del magazzino, la disponibilità del prodotto, e oggi invece puoi chiudere l’ordine autonomamente, significa che io ho perso valore? No, significa che è cambiato il modo di comunicare. Stiamo esplorando nuove modalità e potremo anche scoprire che la soluzione adottata non è la migliore. Tuttavia, saremo pronti a intraprendere nuovi percorsi.

Ricordate quando abbiamo parlato di Architetture Evolutive? Una delle sfide principali è abbracciare una cultura della sperimentazione, in cui fallire è interpretato come provare e non spaventa.

Detto questo, non è sempre facile ignorare i confini esistenti nelle organizzazioni, e non è detto che farlo sia sempre vantaggioso. Procedere per piccoli passi durante un refactoring supporta l’apprendimento del dominio e permette a chi ci chiede supporto di accettare nuovi modelli di comunicazione e condivisione.

Conclusioni

La comunicazione rappresenta la base delle relazioni, non solo tra persone, ma anche tra i sistemi che sviluppiamo. Senza una comunicazione efficace, entrambi sono destinati a fallire, seppur in modi diversi.
Le strutture di comunicazione influenzano il modo in cui il software viene progettato, sviluppato e gestito durante il suo ciclo di vita, così come il ciclo di vita degli utenti che lo utilizzano.

Le informazioni che scegliamo di condividere tra i nostri sistemi, proprio come accade nella vita reale, determinano il livello di indipendenza o di dipendenza di ciascun servizio rispetto agli altri. Questo è un aspetto fondamentale da considerare quando si interviene sulla ristrutturazione di un’applicazione esistente.

Non è realistico pensare di rimodellare tutto in un’unica soluzione: i piccoli cambiamenti sono più facili da accettare e da gestire.

Nel prossimo episodio parleremo del concetto di antifragilità nelle architetture software e nell’agilità.

Articolo scritto da