Domain-Driven Design (DDD) nei microservizi
Quando le organizzazioni passano da un’architettura monolitica ai microservizi, si trovano ad affrontare una domanda critica e ad alto rischio: Come tracciamo i confini dei nostri servizi?
In teoria, i microservizi dovrebbero essere unità sciolte e disaccoppiate che possono essere sviluppate, distribuite e scalate in modo indipendente. In pratica, tuttavia, molti team finiscono per costruire un monolite distribuito, un sistema in cui i servizi sono così strettamente associati che un singolo cambiamento aziendale richiede la modifica e l’implementazione di più servizi contemporaneamente, aggravando la latenza di rete e gli ostacoli di distribuzione.
Per evitare questo errore, gli architetti software si rivolgono al Domain-Driven Design (DDD). Introdotta per la prima volta da Eric Evans nel 2003, DDD è una metodologia di sviluppo software che allinea le strutture del codice ai complessi domini aziendali che rappresentano.
In questo articolo esploreremo il modo in cui DDD fornisce i modelli strategici e tattici per la progettazione di microservizi puliti, disaccoppiati e altamente manutenibili.
1. Il progetto strategico: tracciare i confini del servizio
Il DDD è diviso in due fasi principali: Progettazione strategica e Progettazione tattica. La progettazione strategica riguarda la modellazione dei contesti aziendali e la definizione dei confini dei servizi. Fornisce la visione “macro” dell’architettura.
A. Linguaggio ubiquo
All’interno di un’azienda, reparti diversi usano le stesse parole per significare cose completamente diverse. Ad esempio:
- Per il team di vendita, un “Utente” è un lead o un potenziale cliente.
- Per il team di sicurezza, un “Utente” è un insieme di credenziali di accesso.
- Per il team di spedizione, un “Utente” è l’indirizzo fisico del destinatario.
Cercare di costruire un modello di database singolo e unificato per un “Utente” che soddisfi tutti si traduce in una base di codice enorme e intricata. DDD risolve questo problema stabilendo un linguaggio onnipresente, un vocabolario condiviso definito e utilizzato sia dagli esperti del settore aziendale che dagli sviluppatori all’interno di un confine specifico.
B. Contesti delimitati
Un Contesto Delimitato è il confine esplicito entro il quale si applica un modello di dominio. All’interno del confine, tutti i termini della Lingua Ubiquita hanno un significato unico e inequivocabile.
Invece di un singolo modello “Utente”, definiamo modelli separati all’interno dei rispettivi Contesti Delimitati:
- Nel Contesto dell’ordine, abbiamo un modello
Ordercontenente le informazioni di contatto del cliente. - Nel Contesto di identità, abbiamo un modello
Credentials. - Nel Contesto di spedizione, abbiamo un modello
DeliveryAddress.
La regola d’oro dei microservizi: Un contesto delimitato si associa direttamente a un microservizio.
Suddividendo il sistema in contesti delimitati, garantiamo che i microservizi siano liberamente accoppiati e rappresentino capacità aziendali distinte.
C. Mappatura del contesto: come comunicano i servizi
I contesti delimitati non esistono isolatamente; devono interagire. Una mappa del contesto definisce le relazioni e i meccanismi di traduzione tra i contesti. I modelli chiave includono:
- Kernel condiviso: due contesti condividono un piccolo sottoinsieme del modello di dominio e del database (solitamente sconsigliato nei microservizi a causa dell’accoppiamento).
- Cliente-Fornitore: Un contesto (fornitore) deve fornire dati a un altro (cliente). Il fornitore deve coordinare i programmi di rilascio con il cliente.
- Anti-Corruption Layer (ACL): un livello di traduzione che traduce i dati in ingresso da un sistema esterno nel modello di dominio interno del cliente, impedendo ai modelli esterni di inquinare l’architettura interna.
2. Il progetto tattico: strutturare la base di codice dei microservizi
Una volta tracciati i confini del servizio utilizzando la progettazione strategica, il Tactical Design fornisce una serie di modelli di progettazione per strutturare il codice all’interno di un singolo microservizio.
A. Entità e oggetti valore
All’interno di un microservizio, i modelli sono suddivisi in due categorie:
- Entità: oggetti che hanno un’identità univoca che persiste nel tempo, anche se i loro attributi cambiano. Gli esempi includono
OrderoProduct. - Oggetti valore: oggetti che non hanno un’identità univoca e sono definiti interamente dai loro attributi. Sono immutabili. Gli esempi includono
ShippingAddressoProductDimensions. Se due oggetti valore hanno gli stessi attributi, sono considerati uguali.
B. Aggregati e radici di aggregati
Un Aggregato è un cluster di entità e oggetti valore associati che vengono trattati come una singola unità per le modifiche dei dati. Ogni aggregato ha una Radice aggregata, che è l’unico punto di ingresso attraverso il quale gli oggetti esterni possono interagire con l’aggregato. La radice garantisce che tutte le varianti aziendali (regole) vengano applicate.
Ad esempio, nel diagramma sopra:
- Nel Servizio ordini,
Orderè la radice aggregata. ContieneOrderItem(Entità) eShippingAddress(Oggetto valore). I servizi esterni non possono modificare direttamenteOrderItem; devono chiamare un metodo sulla rootOrder(ad esempio,order.AddItem()), che verifica che l’ordine non sia già stato spedito.
C. Eventi del dominio
Un Evento di dominio è qualcosa che si è verificato nel dominio che interessa agli esperti aziendali. Viene utilizzato per comunicare le modifiche tra contesti delimitati in modo asincrono.
Quando un aggregato esegue un comando, pubblica un evento di dominio (ad esempio, OrderCreated). Altri servizi ascoltano questo evento e aggiornano il proprio stato di conseguenza, garantendo la coerenza finale senza dipendenze API sincrone.
3. Un esempio concreto: servizio di ordine e servizio di inventario
Esaminiamo un’implementazione semplificata del DDD tattico in Go for the Order Service, che mostra come la radice aggregata coordina le regole aziendali.
package domain
import (
"errors"
"time"
)
// Value Object (Immutable, identity-less)
type ShippingAddress struct {
Street string
City string
ZipCode string
}
// Entity (Has identity, mutable)
type OrderItem struct {
ProductID string
Quantity int
Price float64
}
// Aggregate Root (Enforces transactional boundaries)
type Order struct {
ID string
Items []OrderItem
Address ShippingAddress
Status string
CreatedAt time.Time
}
// NewOrder creates a new Order Aggregate Root
func NewOrder(id string, address ShippingAddress) *Order {
return &Order{
ID: id,
Items: []OrderItem{},
Address: address,
Status: "PENDING",
CreatedAt: time.Now(),
}
}
// AddItem enforces business rules before mutating state
func (o *Order) AddItem(productID string, qty int, price float64) error {
if o.Status != "PENDING" {
return errors.New("cannot add items to a finalized or cancelled order")
}
if qty <= 0 {
return errors.New("quantity must be greater than zero")
}
o.Items = append(o.Items, OrderItem{
ProductID: productID,
Quantity: qty,
Price: price,
})
return nil
}
Quando l’ordine viene salvato con successo, il servizio pubblica un evento su un broker di messaggi:
{
"event_id": "evt_98231",
"event_type": "OrderCreated",
"timestamp": "2026-06-26T00:15:00Z",
"payload": {
"order_id": "ord_5521",
"items": [
{ "product_id": "prod_88", "quantity": 2 }
]
}
}
Il Servizio di inventario ascolta questo evento, aggiorna la sua entità StockLevel locale e completa il flusso in modo asincrono.
4. DDD strategico e tattico nei microservizi
| Fase/Aspetto | DDD strategico | DDD tattico |
|---|---|---|
| Ambito | Architettura del sistema globale (macro) | All’interno di un unico codebase di servizio (micro) |
| Obiettivo primario | Disegna confini di servizio puliti e disaccoppiati | Modella una logica aziendale ricca e applica gli invarianti |
| Concetti chiave | Contesti delimitati, linguaggio ubiquo, mappa contestuale | Entità, oggetti valore, aggregati, eventi di dominio |
| Pubblico | Architetti, Product Manager, Sviluppatori | Sviluppatori di software, revisori di codici |
| Impatto sui microservizi | Determina il numero e la portata dei servizi | Determina le transazioni del database e la struttura delle directory |
Conclusione: inizia prima con il DDD strategico
L’applicazione della progettazione basata sui domini ai microservizi è un modo efficace per garantire che la tua architettura corrisponda alla struttura aziendale. Tuttavia, i team spesso commettono l’errore di concentrarsi troppo su modelli tattici (come scrivere interfacce di repository e oggetti di valore) ignorando la progettazione strategica.
Se non si definiscono correttamente i confini, nessuna quantità di codice tattico pulito potrà evitare che il sistema si trasformi in un monolite distribuito. Inizia sempre con la mappatura strategica: definisci il tuo linguaggio ubiquo, raggruppa la logica in contesti delimitati, mappa i flussi di comunicazione e lascia che i confini dei tuoi microservizi emergano naturalmente da tali definizioni.