DDD, microservizi e architetture evolutive: pattern strategici
Parlando di DDD e architetture, nel precedente articolo abbiamo visto la distinzione tra problem space e solution space.
I microservizi, all’inizio di un progetto, possono essere troppo costosi. Anche rifattorizzare un sistema esistente può risultare complesso e oneroso. D’altra parte, il monolite può rallentare la scalabilità quando il sistema cresce e le esigenze cambiano.
La modular architecture, o architettura modulare, sembra offrire il meglio di entrambi i mondi, senza gli svantaggi più evidenti.
Prima di vedere come adottarla, cerchiamo di capire cosa si intende per modulo e perché questo concetto si sposa con il Domain-Driven Design (a questo link puoi leggere l’articolo originale, pubblicato nella rivista web MokaByte).
Module
Un modulo racchiude un insieme di oggetti che risolvono un concetto di business ben definito. Quegli oggetti sono fortemente coesi e lavorano strettamente insieme. L’alto livello di accoppiamento interno non è un problema: non separeremo mai quegli oggetti, perché formano un’unità funzionale coerente e autosufficiente.
Il punto critico è mantenere i moduli indipendenti tra loro. Ogni modulo risolve un problema diverso, anche se fa parte dello stesso sistema. Serve quindi un basso accoppiamento tra i moduli. Solo così potremo, in futuro, estrarli come microservizi senza dover ripensare tutto.
Questa è la forza della modularità: ci prepara all’evoluzione, senza obbligarci a decidere tutto fin dall’inizio.
Modular architecture e Domain-Driven Design
A questo punto è naturale chiedersi come questa architettura si intrecci con il DDD. Fermiamoci un attimo e osserviamo i pattern coinvolti.
Eric Evans, nel suo libro iconico “Domain-Driven Design: Tackling Complexity in the Heart of Software” conosciuto come Blue Book, propone diversi pattern pensati per sviluppare software orientato al business, più che semplicemente al dato. Li divide in due categorie principali: Pattern Strategici e Pattern Tattici.
Entrambi offrono strumenti preziosi in contesti differenti. I pattern strategici ci aiutano a costruire la big picture del sistema, suddividendo l’intero dominio in più sottodomini distinti.
Business domain & ubiquitous language
Partiamo da un chiarimento importante: il dominio non è per forza qualcosa di legato al software. Anzi, spesso non lo è affatto. Il dominio rappresenta l’attività principale di un’azienda. È il servizio che offre, ciò che la distingue dai suoi concorrenti nel mercato.
Pensiamo a Uber. Il suo dominio è offrire un servizio di trasporto privato, semplice da usare sia per chi guida che per chi viaggia. Il dominio non è un problema informatico. Ma grazie a un’app ben progettata, ne cogliamo il valore concreto.
Secondo la definizione di Domain-Driven Design, il dominio è la sfera di conoscenza e attività su cui si fonda la logica dell’applicazione.
Software e business: come parlare la stessa lingua?
Business e software parlano due lingue diverse. Come farli dialogare senza creare confusione e soluzioni sbagliate? L’idea di Eric Evans è creare un modello comune. Un ponte tra sviluppatori e stakeholder, basato su un linguaggio chiaro e condiviso. Un modello non è la copia perfetta del mondo reale. È uno strumento per comprendere e affrontare un dominio complesso. Più il modello si avvicina alla realtà, più le decisioni che ne derivano saranno efficaci e mirate alla soluzione del problema.
Per costruire questo modello condiviso, serve un linguaggio preciso. Senza ambiguità, chiaro per tutti: tecnici, clienti e utenti. Questo linguaggio condiviso è il primo pattern strategico proposto nel Blue Book: si chiama ubiquitous language.
Ubiquitous language
Durante lo sviluppo software, il linguaggio usato dagli esperti di dominio viene spesso “tradotto” in termini tecnici, per essere più comprensibile agli sviluppatori. Questa traduzione avviene nei documenti di requisiti, che però raramente riescono a catturare la vera natura del problema di business. Ogni volta che un termine passa da un contesto a un altro, perde sfumature importanti: non solo semantiche, ma anche comportamentali. L’intenzione è buona, certo. Ma il risultato è un modello impoverito della realtà, che porta a soluzioni incomplete o, peggio, del tutto sbagliate.
Trovare un linguaggio comune, che elimini le ambiguità, è fondamentale. Serve per allinearsi davvero su cosa intendiamo, ad esempio, per caffè. Perché proprio caffè? Perché, da italiani, quando lo ordiniamo all’estero, sappiamo che qualcosa andrà storto. Il barista capisce la parola, ma non il significato che le diamo noi. Ci aspettiamo un espresso ristretto, forte, servito in tazzina. Riceviamo invece una bevanda lunga, annacquata, che ha solo il colore del nostro caffè. Il significato si è perso nella traduzione.
Ecco perché non basta avere un dizionario condiviso. Serve un linguaggio vero, rigoroso come quello tecnico, ma comprensibile a tutti. Quando diciamo che bisogna restare nel problem space il più a lungo possibile, intendiamo proprio questo: costruire un linguaggio chiaro e condiviso. Solo così chi partecipa alla soluzione potrà comprendere davvero il problema, e descriverlo senza ambiguità.
Ma allora, è sufficiente imparare il linguaggio del Dominio? No, purtroppo non basta.
Domain language vs ubiquitous language
In molte interpretazioni del Domain-Driven Design, il domain language viene spesso confuso con l’ubiquitous language. Ma c’è una differenza importante.
Il domain language è il linguaggio naturale usato ogni giorno da chi lavora all’interno del dominio. È spontaneo, spesso impreciso, pieno di impliciti.
Come diceva Giorgio Gaber:
“Le parole definiscono il mondo, se non ci fossero le parole, non avremmo la possibilità di parlare di niente. Ma il mondo gira, e le parole stanno ferme, si logorano, invecchiano, perdono di significato, perdono di senso, e tutti noi continuiamo ad usarle, senza accorgerci di parlare di niente”
Non ricorderemo Gaber per il suo contributo al software, ma questa frase parla chiaramente anche a noi sviluppatori. Nel nostro lavoro, usare parole logore e ambigue è un problema enorme. Non possiamo permettercelo.
A differenza del linguaggio naturale, l’ubiquitous language nasce con uno scopo preciso: eliminare l’ambiguità e rendere esplicita la logica del dominio. È un linguaggio costruito insieme, da stakeholder e progettisti, per supportare la progettazione del sistema. Deve essere chiaro, condiviso, coerente e preciso: ogni termine ha un solo significato. Non serve solo a descrivere proprietà, ma anche i comportamenti del dominio. L’ambiguità ostacola la comunicazione, crea incomprensioni, e porta a errori. Deve eliminare la necessità di fare ipotesi e rendere esplicita la logica del dominio aziendale. Per questo ogni parola deve essere scelta con cura. Una volta chiarito cos’è l’ubiquitous language, possiamo metterlo in pratica.
Rebecca Wirfs-Brock dice:
“A model is a simplified representation of a thing or phenomenon that intenionally emphasizes certain aspects while ignoring others. Abstractions with a specific use in mind.”
George Box aggiunge:
“All models are wrong, but some are useful.”
Non esiste un modello universale o un linguaggio universale per l’intero dominio. Esistono solo linguaggi adatti a uno specifico problema di business. È come con le mappe: ne esistono tante, ognuna utile per un’esigenza diversa.

È evidente che nessuna delle mappe disponibili può rappresentare l’intero pianeta Terra. Al contrario, ognuna di esse contiene solo i dettagli strettamente necessari allo scopo per cui è stata progettata. Di conseguenza, per ciascuna mappa utilizziamo un linguaggio specifico, adatto al contesto e al problema da risolvere.
Abbiamo bisogno di costruire modelli astratti della realtà per gestire la complessità. L’astrazione ci consente di omettere ciò che non è rilevante e concentrarci solo sugli elementi significativi.
Ma cosa c’entra tutto questo con l’ubiquitous language? Quando definiamo un linguaggio adatto al dominio del problema che vogliamo risolvere, stiamo creando un modello concettuale. Questo modello riflette un sotto-dominio dell’azienda. L’obiettivo è catturare i modelli mentali e i processi degli esperti di dominio per tradurli in funzionalità software.
Il linguaggio, e quindi il modello, deve rappresentare in modo fedele le entità aziendali coinvolte, i loro comportamenti, le relazioni reciproche, le cause-effetto e gli invarianti. Approfondiremo questi aspetti più avanti.
Un altro punto essenziale è l’evoluzione. Proprio come ricordava Gaber, anche l’ubiquitous language cambia nel tempo. I motivi sono molteplici: il mercato si trasforma, i bisogni evolvono, e i significati dei termini si adattano al presente. Per questo motivo è cruciale mantenere un dialogo costante con gli esperti di dominio ed evitare supposizioni.
Consolidare il linguaggio significa ridurre il debito tecnico rispetto alla conoscenza del dominio. Significa anche costruire soluzioni più aderenti alla realtà aziendale. Una volta definito un linguaggio condiviso, questo va adottato ovunque: nella documentazione, nel codice e in ogni artefatto del progetto. Solo così si evitano ambiguità e incomprensioni.
Quando il linguaggio comune del dominio è stato definito, possiamo suddividerlo in linguaggi più piccoli, ciascuno legato a un contesto specifico del business. In questo modo emergono i Bounded Context, o se preferisci, i moduli.
Come si identificano questi confini? Cercando i punti in cui uno stesso termine assume significati diversi in contesti differenti. Prendiamo l’esempio di un articolo: per la logistica contano l’ubicazione, le scorte e i punti di riordino. Per il reparto commerciale, invece, sono importanti prezzo, margine e tempi di consegna. Parliamo dello stesso oggetto, ma da due Bounded Context distinti.
Bounded Context
Lo abbiamo già detto: il modello che usiamo per sviluppare un sistema non è la copia della realtà. È un costrutto utile per dare significato a un contesto complesso. È bene chiarirlo: questo modello non sarà valido per sempre, per tutta la vita del sistema.
Le esigenze cambieranno. Nuove richieste arriveranno dal mercato. Nessuno, all’inizio del progetto, può prevederle. Non possiamo farlo noi sviluppatori, neppure basandoci “sulla nostra esperienza”, perché ogni progetto ha una storia a sé. Non può farlo nemmeno il cliente che ci commissiona il software.
Aggiungere proprietà a un modello solo “perché prima o poi potrebbero servire” è una cattiva abitudine. È diffusa tra chi scrive software e, paradossalmente, più siamo senior, più ci sentiamo legittimati a farlo.
No. Tutte le buone pratiche di sviluppo ci insegnano un principio fondamentale: realizzare solo ciò che serve per risolvere il problema attuale. Tutto ciò che va oltre è accoppiamento inutile verso un modello errato. E prima o poi, lo pagheremo caro.
Quindi? Dobbiamo accettarlo: il modello universale, la silver bullet, non esiste. Ogni modello ha bisogno di confini precisi. Superato quel limite, la sua rappresentazione della realtà perde validità.
Per chi ama la filosofia, questo concetto richiama il platonic fold:
“La piega platonica è il confine esplosivo dove il modello platonico entra in contatto con la realtà disordinata, dove il divario tra ciò che si sa e ciò che si pensa di sapere diventa pericolosamente ampio”.
I Bounded Context definiscono il perimetro entro cui un sottoinsieme del nostro ubiquitous language e del modello ha senso. Consentono di costruire modelli distinti in base ai diversi domini di problema.
Terminologia, regole e concetti espressi all’interno di un linguaggio hanno valore solo nel contesto in cui sono stati definiti. È proprio quello che accade nell’esempio dell’articolo menzionato in precedenza.
Bounded Context vs sottodomini
È importante chiarire la distinzione tra Bounded Context e sottodominio. A prima vista possono sembrare equivalenti, ma si collocano su piani differenti: uno riguarda il business, l’altro la progettazione del sistema.
Per comprendere la strategia di un’azienda, è necessario analizzarne il dominio. Come previsto dai pattern del Domain-Driven Design, questa analisi porta all’identificazione di più sottodomini. È così che scopriamo come l’organizzazione opera e pianifica le proprie azioni sul mercato.
I Bounded Context rappresentano nel sistema software il risultato di tale analisi. Sono progettati da noi sviluppatori e riflettono scelte strategiche, motivo per cui rientrano nei pattern strategici del DDD. La loro funzione è suddividere il modello di dominio in componenti più semplici da gestire.
Stabilire se a ogni sottodominio debba corrispondere un singolo Bounded Context o più d’uno dipende dalla complessità in gioco. Una mappatura uno-a-uno può essere del tutto appropriata in certi scenari. In altri, potrebbero servire strategie di decomposizione più articolate.
Nel software design, non esistono regole assolute. Ogni decisione è un compromesso tra benefici e costi. Sta a noi valutare la strategia migliore per il contesto specifico. Non conta solo come progettiamo una soluzione, ma soprattutto perché adottiamo una scelta piuttosto che un’altra.

Tipi di sottodominio
Progettare un sistema software con un approccio orientato al business richiede una distinzione precisa tra i tipi di sottodominio emersi durante l’analisi. Proprio come accade nei reparti di un’azienda, non tutti i sottodomini hanno lo stesso peso strategico. Dipende dalla natura del business. Anche nel software, questa differenziazione deve riflettersi nel progetto.
Nel Domain-Driven Design, i sottodomini vengono classificati in tre categorie principali:
Core subdomains
Sono i sottodomini che distinguono l’azienda dai concorrenti. Qui si concentra la vera innovazione e il vantaggio competitivo. Per questo motivo, rappresentano l’area più strategica anche per lo sviluppo software.
Su questi sottodomini si investono le risorse migliori: team esperti, pratiche di sviluppo avanzate, uso sistematico dei pattern. La ragione è semplice: un core subdomain facile da realizzare può offrire un vantaggio solo temporaneo. Per essere davvero efficace, deve affrontare e risolvere problemi complessi.
Generic subdomains
Rientrano in questa categoria i sottodomini comuni a tutte le aziende, che non generano alcun vantaggio competitivo. Esempi tipici sono le funzionalità di autenticazione o autorizzazione. In questi casi, è spesso conveniente affidarsi a soluzioni esterne già collaudate, anziché sviluppare componenti ad hoc.
Supporting subdomains
Come suggerisce il nome, questi sottodomini hanno una funzione di supporto al core business. Non offrono vantaggi strategici diretti, ma sono essenziali per il corretto funzionamento dell’intero sistema.
Spesso non esistono soluzioni preconfezionate per soddisfare le esigenze specifiche di questi ambiti. Di conseguenza, vengono generalmente sviluppati in casa, pur restando in secondo piano rispetto ai core subdomains
Integration patterns
Abbiamo visto come la suddivisione in sottodomini di un modello di business ci consenta di ottenere diversi modelli che possono evolvere indipendentemente gli uni dagli altri, al di là del fatto che ci lavori un solo team o diversi team. Questa proprietà dei Bounded Context è ciò che li ha eletti a pattern per la definizione dello scopo di un microservizio. Detto questo i bounded context non sono di per sé indipendenti. Esattamente allo stesso modo per cui un sistema non è l’insieme dei singoli componenti, ma il modo in cui questi sono assemblati e interagiscono fra loro, allo stesso modo il nostro software non è semplicemente l’insieme dei bounded context che abbiamo identificato, ma il modo in cui questi comunicano, e si scambiano informazioni fra loro. Di conseguenza ci saranno sempre dei punti di contatto tra un bounded context ed un altro, e questi punti di contatto, così come nella vita reale, saranno governati da contratti.
La ragione per cui abbiamo bisogno di definire dei contratti per far dialogare fra loro diversi bounded context è semplice, in ogni bounded context è valido uno specifico ubiquitous language, che non è valido per gli altri, altrimenti non li avremmo divisi. Nel momento in cui sorge la necessità di integrare delle informazioni è fondamentale decidere quale linguaggio adottare per l’integrazione stessa. Nel Domain-Driven Design questa problematica è affrontata dal pattern strategico Context Mapping, che suddivide le tipologie di comunicazione in tre gruppi, che rappresentano il tipo di collaborazione in essere: cooperation, customer-supplier, separate ways.
Cooperation
La collaborazione di tipo Cooperation riguarda bounded context che hanno una relazione stretta e continuativa. Il successo di uno dipende direttamente dal successo dell’altro, e viceversa. Esiste una comunicazione stabile e condivisa, fondata su obiettivi comuni.
In questo scenario, i team coinvolti lavorano a stretto contatto e concordano insieme le regole di integrazione. Appartengono a questo gruppo diversi pattern, che vedremo nei paragrafi successivi.
Partnership
In questo modello, l’integrazione è gestita in modo ad hoc. Un team notifica all’altro le modifiche apportate al proprio modello, e il secondo team si adatta senza conflitti alle nuove modifiche. L’integrazione avviene in entrambe le direzioni: nessun team o bounded context impone le regole, ma ogni parte lavora indipendentemente, mantenendo un flusso armonioso di informazioni.
Questo tipo di collaborazione richiede una continua sincronizzazione e un elevato livello di comunicazione. Pertanto, è più efficace quando i team sono geograficamente vicini, facilitando così il dialogo e l’allineamento rapido.
Shared kernel
Quando una parte di un sottodominio è condivisa tra più sottodomini, può essere utile creare un modello condiviso tra i diversi bounded context. Tuttavia, è fondamentale che il modello condiviso rimanga consistente in tutti i context coinvolti. Questo rappresenta un accoppiamento tra i contesti, e come sempre, bisogna valutarne i pro e contro prima di procedere.
Esistono delle linee guida che possono aiutarci a prendere questa decisione. Ecco un breve elenco di domande da considerare:
- Il consumo del modello condiviso porterà a una perdita di capacità?
- I consumatori acquisiranno nuove capacità?
- Potranno concentrarsi maggiormente sulle loro scelte strategiche?
- La dipendenza dal modello condiviso rallenterà il processo?
- I consumatori avranno la possibilità di rifiutare la migrazione al modello condiviso?
- Il team di sviluppo del modello condiviso sarà sufficientemente reattivo?
- Il numero di modelli dipendenti diventerà problematico?
- Il costo di migrazione sarà troppo elevato?
Come sempre, non esistono leggi universali: la scelta dipende dal contesto specifico.
Customer-supplier
Il secondo gruppo di pattern di collaborazione è il customer-supplier. Come mostrato nell’immagine, in questo modello, uno dei bounded context (il supplier) fornisce un servizio a un altro bounded context (il customer).
A differenza del pattern cooperation, dove il successo di entrambi i team è interdipendente, in questo caso i team upstream e downstream possono avere successo indipendentemente l’uno dall’altro. Questo spesso porta a uno squilibrio di potere, dove uno dei team (solitamente il supplier) può dettare le regole per il contratto di integrazione.
I seguenti pattern appartengono a questo gruppo.
Conformist
In alcuni casi, il team upstream non ha alcuna motivazione per supportare i team downstream, limitandosi a fornire un contratto di integrazione che gli altri devono semplicemente accettare (prendere o lasciare). Questo avviene quando ci integriamo con un servizio esterno, su cui non abbiamo alcun potere di negoziazione.
Se il team downstream è costretto ad accettare il contratto di integrazione così com’è, il rapporto tra i due team è definito conformist.
Anticorruption layer
Nel pattern Anticorruption Layer, è ancora il team upstream a imporre le regole del contratto di integrazione, ma, a differenza del pattern conformist, in questo caso il team downstream non può accettare il contratto per diverse ragioni: cambiamenti frequenti, inefficienza nella gestione del contratto, o la necessità di proteggere il proprio bounded context.
L’Anticorruption Layer funge da intermediario tra il contratto esterno e il nostro bounded context, traducendo e adattando le regole esterne per garantire l’integrità e l’indipendenza del contesto interno.
Open-host service
Nel pattern Open-Host Service, il potere decisionale è sbilanciato verso chi sta a monte, ma a differenza del pattern precedente, è proprio il team upstream a proteggere i propri consumatori separando l’implementazione interna del contratto da quella esterna. Questo disaccoppiamento consente al bounded context a monte di evolvere il proprio modello di implementazione senza influenzare l’integrazione esterna.
In alcuni casi, potrebbe essere necessario mantenere più versioni del contratto esterno per consentire ai consumatori di adattarsi alle nuove specifiche. In questa situazione, il bounded context a monte può esporre più versioni del contratto di integrazione. Un esempio classico è rappresentato dall’esposizione di più versioni di API REST da parte di un bounded context.
Separate Ways
Esiste un terzo gruppo di collaborazione che non prevede alcun tipo di interazione. Vediamo i casi che rientrano in questo gruppo.
Communication Issues
Quando i team incontrano difficoltà nel comunicare o collaborare, anche per ragioni politiche interne all’organizzazione, può essere conveniente che ciascuno vada per la propria strada. In questi casi, può risultare preferibile duplicare alcune funzionalità in più bounded context.
Generic Subdomains
Esistono funzionalità che sono più facili da duplicare che non da integrare, soprattutto nei generic subdomains, che sono solitamente facili da sviluppare o da acquistare e integrare. Un esempio tipico è il logging.
Model Differences
Quando le differenze tra i modelli di diversi bounded context sono troppo evidenti, può essere più conveniente duplicare le funzionalità piuttosto che investire tempo nell’integrazione. Questo approccio non è consigliabile, tuttavia, quando si tratta di core subdomains, in quanto duplicare funzionalità a questo livello potrebbe contrastare la strategia aziendale, che punta a ottimizzare i processi concentrando gli sforzi principali proprio su questi sottodomini.
Context map
Context mapping è una rappresentazione visiva dei bounded context e delle relazioni tra di essi, che fornisce preziose indicazioni strategiche. In particolare, offre una panoramica dei componenti e dei modelli implementati (High-level design), ma anche una visione delle modalità di collaborazione tra i team, evidenziando quelli che lavorano insieme e quelli che preferiscono seguire strade autonome (Communication patterns). Inoltre, permette di individuare eventuali problemi organizzativi, ad esempio quando si nota che tutti i rapporti di collaborazione sono del tipo Separate Ways o Anticorruption Layer.
Conclusioni
Partendo dall’architettura modulare, abbiamo visto come il Domain-Driven Design supporti l’aspetto strategico dello sviluppo software. Sebbene l’applicazione di questi pattern venga spesso associata alla realizzazione di sistemi a microservizi, come abbiamo osservato, non è affatto una necessità. Come ripeto spesso, nel 2003, anno di pubblicazione del libro di Eric Evans, i microservizi non erano nemmeno un concetto, e l’architettura principale era la layer architecture. Tuttavia, tutti i pattern esplorati sono applicabili in qualsiasi contesto.
Per un esempio pratico dell’applicazione di questi concetti, potete consultare questo progetto.
Il mio personale trattato sul mondo del Domain-Driven Designprosegue nel prossimo articolo con un approfondimento sulle architetture evolutive, o evolutionary architecture.