Come si scrive il Domain-Driven Design in C#?
Entity Framework Core mappa il modello da fuori, e i test del dominio girano senza database.

Elena lavora da tre anni in un'azienda di Bergamo che produce guarnizioni industriali. Il gestionale ordini è suo: una soluzione .NET con un progetto web, uno di accesso ai dati e una cartella Models piena di classi con proprietà pubbliche e nessun metodo. Un martedì il commerciale le chiede una regola semplice: un ordine confermato non si può più modificare, e un cliente che ha superato il fido non può confermare nuovi ordini. Elena apre il codice e scopre che la regola dovrebbe stare in sette punti diversi: due controller, un'importazione da Excel, un servizio di fatturazione, un'API per l'app dei venditori e due pagine che nessuno ricorda di aver scritto.
Ne dimentica una. Tre settimane dopo un ordine confermato viene modificato da un'importazione notturna, la fattura esce con un importo sbagliato e il commerciale chiama l'amministrazione. Elena non ha scritto codice cattivo. Ha scritto codice in cui le regole del business non hanno una casa: ogni punto che tocca un ordine può fare qualsiasi cosa a un ordine.
Il Domain-Driven Design, in sigla DDD, nasce per dare una casa a quelle regole. Se cerchi che cosa sia a livello concettuale, trovi la spiegazione completa nella guida al Domain Driven Design e a come funziona. Questo articolo fa il passo successivo: prende le idee e le trasforma in codice C# su .NET 10, con entità che proteggono le proprie regole, oggetti valore scritti con i record, aggregati con un confine chiaro, mappatura con Entity Framework Core, eventi di dominio e test.
Scrivo software dal 1999 e in aula vedo sempre lo stesso schema: chi applica il DDD solo nei nomi delle cartelle ottiene cartelle con nomi eleganti e lo stesso disordine di prima. Chi lo applica nelle classi ottiene un modello che si lascia leggere dal commerciale. Qui trovi la seconda strada, passo dopo passo.
Che cos'è il Domain-Driven Design in C# e quando conviene davvero?
Il Domain-Driven Design in C# è un modo di scrivere il codice in cui le regole del business vivono in classi del dominio con comportamento, scritte con le stesse parole che usano gli esperti dell'azienda. Nessun pacchetto NuGet lo contiene: è una disciplina di progettazione descritta da Eric Evans nel 2003, che in .NET prende la forma di classi, record e confini fra progetti.
Che cosa cambia nel codice di tutti i giorni
Nel codice che molti team scrivono ancora, la classe Ordine è un sacchetto di proprietà: Stato, Totale, Righe, tutte con get e set pubblici. Le regole stanno altrove, in servizi che leggono le proprietà, decidono e le riscrivono. Nel DDD succede il contrario: l'ordine espone un metodo Conferma() e solo quel metodo può portarlo nello stato confermato, dopo aver controllato che le righe esistano e che il totale sia coerente. Chi vuole cambiare lo stato deve passare di lì.
La conseguenza si vede alla prima modifica. Quando il commerciale cambia la regola, tocchi un metodo in una classe e hai finito, perché nessun altro punto del codice può scavalcarla. I sette punti di Elena diventano uno.
Il linguaggio condiviso è la parte che si salta più spesso
Il cuore del DDD sta nel vocabolario, prima che nel codice. Se in magazzino dicono "evasione" e nel codice trovi ProcessOrder, ogni conversazione fra chi usa il gestionale e chi lo scrive richiede una traduzione, e le traduzioni perdono pezzi. Nel DDD si siede il commerciale accanto allo sviluppatore, si decide che cosa significa "ordine confermato" e quel termine finisce nel codice, identico: classe Ordine, metodo Conferma, stato Confermato. Evans lo chiama linguaggio onnipresente, e funziona come un contratto: se la parola cambia nel business, cambia nel codice.
Quando conviene e quando no
Il DDD costa tempo all'inizio e lo restituisce quando il dominio è complicato: prezzi con sconti a scaglioni, vincoli di magazzino, workflow di approvazione, regole che cambiano ogni trimestre. Per un'anagrafica con quattro campi, o per un'applicazione che legge e scrive tabelle senza regole, è eccessivo: bastano una pagina di modifica e Entity Framework. Una regola pratica che uso con i clienti: se per spiegare il software a un nuovo collega servono più di dieci regole dette a voce, il DDD ti serve. Se bastano due frasi, risparmia il tempo.
Strategico e tattico: due livelli da non confondere
Il DDD ha una parte strategica, che riguarda i contesti delimitati e le relazioni fra i team, e una parte tattica, che riguarda le classi: entità, oggetti valore, aggregati, servizi di dominio ed eventi. Gli esempi di questo articolo sono quasi tutti tattici, perché è la parte che apri in Visual Studio il lunedì mattina. La parte strategica compare nella sezione sui contesti, più avanti, dove si decide come dividere la soluzione.
Come si scrive un'entità in C# che protegge le proprie regole?
Un'entità è un oggetto con un'identità che dura nel tempo: due ordini con gli stessi dati restano ordini diversi perché hanno numeri diversi. In C# si scrive con setter privati, un costruttore che impedisce stati impossibili e metodi che descrivono le azioni consentite con i nomi del business.
Costruttore e stato iniziale
Un ordine non nasce con uno stato scelto a caso. Nasce in bozza, con un cliente e un numero, e nessuno dall'esterno può inventarsi una combinazione diversa. Il costruttore pubblico richiede i dati obbligatori e lancia un'eccezione se mancano; quello senza parametri, che serve a Entity Framework, resta privato.
public class Ordine
{
private readonly List<RigaOrdine> _righe = new();
public Guid Id { get; private set; }
public StatoOrdine Stato { get; private set; }
public IReadOnlyCollection<RigaOrdine> Righe => _righe;
private Ordine() { } // per Entity Framework
public Ordine(Guid clienteId)
{
Id = Guid.NewGuid();
ClienteId = clienteId;
Stato = StatoOrdine.Bozza;
}
public Guid ClienteId { get; private set; }
}Due dettagli contano più del resto. Le righe sono esposte come IReadOnlyCollection, quindi dall'esterno nessuno può fare ordine.Righe.Add(...) e saltare i controlli. E il costruttore pubblico genera lui l'identificatore, così nessun chiamante può creare due ordini con la stessa chiave.
I metodi hanno i nomi del business
Invece di impostare Stato = Confermato da un controller, l'ordine espone Conferma(). Dentro il metodo vivono le regole, e solo lì: un ordine vuoto non si conferma, un ordine già confermato non si conferma di nuovo, un ordine annullato non torna in vita. Se la regola è violata il metodo lancia un'eccezione di dominio con un messaggio comprensibile, per esempio DominioException("Un ordine senza righe non può essere confermato").
Il nome del metodo è una scelta di progettazione. AggiungiRiga(articolo, quantita, prezzo) dice più di Righe.Add, perché dentro puoi controllare che l'ordine sia ancora in bozza, che la quantità sia positiva e che l'articolo non sia già presente. Il commerciale legge il nome e capisce l'azione senza aprire il corpo.
Identità e uguaglianza
Per le entità l'uguaglianza si basa sull'identificatore e non sui campi. Due oggetti Ordine con lo stesso Id sono lo stesso ordine anche se uno è stato caricato prima di una modifica. Se confronti entità in collezioni o in test, ricordati di sovrascrivere Equals e GetHashCode sull'identificatore, oppure di confrontare direttamente gli Id. Entity Framework Core, dentro il suo DbContext, tiene già traccia dell'identità, quindi il problema si presenta soprattutto fuori dalla persistenza.
Stati e transizioni
Molti ordini passano da bozza a confermato, spedito, fatturato e annullato. Se lo stato è una semplice enumerazione modificabile, ogni salto è possibile. Nel modello a oggetti scrivi le transizioni ammesse come metodi: Conferma parte da bozza, Spedisci parte da confermato, Annulla è consentito solo prima della spedizione. Il diagramma degli stati che il commerciale disegna alla lavagna diventa il codice, riga per riga, e chi legge il codice ritrova la lavagna.
Che cos'è un value object e come si scrive con i record di C#?
Un oggetto valore è un oggetto definito solo dai suoi dati, senza identità propria e immutabile: due importi da 100 euro sono lo stesso importo. In C# i record sono nati per questo, perché danno uguaglianza per valore, immutabilità e una sintassi di copia con with senza codice aggiuntivo.
Perché non basta un decimal
Nel gestionale di Elena l'importo è un decimal sparso in dodici classi. Quando l'azienda comincia a vendere in dollari, nessuno sa quali decimal sono euro e quali no, e la somma di due importi in valute diverse compila senza errori. Un oggetto valore Denaro con importo e valuta risolve il problema alla radice, perché il compilatore smette di accettare la somma di euro e dollari e il metodo Somma lancia un'eccezione chiara se le valute differiscono.
public sealed record Denaro
{
public decimal Importo { get; }
public string Valuta { get; }
public Denaro(decimal importo, string valuta)
{
if (importo < 0) throw new DominioException("L'importo non può essere negativo");
if (string.IsNullOrWhiteSpace(valuta) || valuta.Length != 3)
throw new DominioException("La valuta deve avere tre lettere");
Importo = decimal.Round(importo, 2);
Valuta = valuta.ToUpperInvariant();
}
public Denaro Somma(Denaro altro)
{
if (altro.Valuta != Valuta) throw new DominioException("Valute diverse");
return new Denaro(Importo + altro.Importo, Valuta);
}
}La validazione sta nel costruttore
Un oggetto valore valido per costruzione toglie un intero genere di controlli dal resto del codice. Se hai in mano un Denaro, sai già che l'importo non è negativo e che la valuta ha tre lettere, e nessun metodo successivo deve ricontrollarlo. Lo stesso vale per Email, PartitaIva, Cap, Indirizzo: l'oggetto, una volta creato, non può essere sbagliato. La stringa che arriva da un modulo web è invece sbagliata fino a prova contraria.
Quando un oggetto valore e quando un'entità
La domanda da farsi è semplice: ho bisogno di distinguere due istanze con gli stessi dati? Un cliente con lo stesso nome e la stessa partita IVA di un altro può essere un'altra anagrafica, perché lo storico degli ordini è diverso: è un'entità. Un indirizzo di consegna uguale a un altro è lo stesso indirizzo: è un oggetto valore. Se due istanze identiche possono scambiarsi senza che nessuno se ne accorga, hai un oggetto valore.
Record, record struct e classi sigillate
Con record ottieni uguaglianza per valore, ToString leggibile e il metodo with per creare una copia modificata. Il record struct evita l'allocazione sullo heap e va bene per oggetti piccoli e frequenti come una coordinata. Per un oggetto con regole di costruzione, come Denaro, preferisco un record sigillato con costruttore esplicito: i parametri posizionali brevi sono comodi, ma aggirano il controllo se non li affianchi a una validazione.
Che cos'è un aggregato e come si sceglie dove tracciare il confine?
Un aggregato è un gruppo di entità e oggetti valore che si modifica come un'unica unità, con una radice che fa rispettare le regole e un solo punto d'ingresso dall'esterno. L'ordine con le sue righe è l'esempio classico: nessuno modifica una riga senza passare dall'ordine, perché solo l'ordine conosce il totale, lo stato e i limiti.
La radice come unico punto d'ingresso
Dall'esterno si carica l'ordine, si chiama ordine.AggiungiRiga(...) e si salva. Non esiste un repository delle righe, perché una riga non ha senso fuori dal suo ordine. Questa regola evita il guaio più comune dei gestionali: la riga modificata direttamente dal database da un servizio che non sapeva che l'ordine era già confermato.
La radice protegge anche gli invarianti, cioè le condizioni che devono essere vere ogni volta che l'aggregato viene salvato. Nell'ordine, il totale deve coincidere con la somma delle righe e le righe confermate non cambiano più. Un invariante che vale solo "di solito" resta un augurio.
Un aggregato piccolo è un aggregato che funziona
L'errore tipico è mettere nell'aggregato tutto ciò che è collegato: l'ordine contiene il cliente, che contiene tutti gli ordini, che contengono tutte le righe, e a ogni modifica carichi mezzo database. La regola che applico è questa: dentro l'aggregato stanno solo gli oggetti che devono essere coerenti nello stesso istante. L'ordine e le sue righe sì, perché il totale deve tornare subito. Il cliente no: l'ordine lo conosce tramite il suo Guid, non tramite un riferimento all'oggetto.

Riferimenti per identificatore, non per oggetto
Fra un aggregato e l'altro si usa l'identificatore. L'ordine ha ClienteId, non un oggetto Cliente. Il vantaggio è triplice: ogni aggregato si carica e si salva per conto suo, i confini restano visibili nel codice e in futuro, se il cliente passa a un altro servizio, il modello non si rompe. Quando serve il nome del cliente per una schermata, lo ricavi con una query di lettura dedicata, senza caricare l'aggregato.
Una transazione, un aggregato
Di norma una transazione modifica un solo aggregato. Se un'operazione ne richiede due, per esempio confermare un ordine e ridurre il fido del cliente, la strada pulita è confermare l'ordine e comunicare l'evento "ordine confermato", che aggiorna il fido in un secondo momento. È la coerenza finale, e nei gestionali la si accetta più facilmente di quanto si creda: il commerciale non nota se il fido scende dopo 40 millisecondi.
Se ti accorgi che una regola richiede di modificare due aggregati nello stesso istante, hai due possibilità: i confini sono sbagliati e i due oggetti vanno uniti, oppure la regola ammette un breve ritardo. Il caso va discusso con il business prima di toccare il codice.
Come si mappano entità e value object con Entity Framework Core?
Entity Framework Core 10 mappa le entità con costruttori privati e campi di supporto, e gli oggetti valore con ComplexProperty oppure con OwnsOne. Il modello di dominio resta pulito e la configurazione vive in una classe separata, nel progetto di accesso ai dati.
Configurare senza sporcare il dominio
Il dominio non deve conoscere Entity Framework: niente attributi [Key] o [Column] sulle classi dell'ordine. La mappatura sta in una classe OrdineConfiguration che implementa IEntityTypeConfiguration<Ordine>. Qui dici che la chiave è Id, che lo stato si salva come stringa leggibile e che la collezione delle righe usa il campo privato _righe. Entity Framework riempie i campi privati e chiama il costruttore privato senza parametri, quindi il dominio non perde nessuna protezione.
public class OrdineConfiguration : IEntityTypeConfiguration<Ordine>
{
public void Configure(EntityTypeBuilder<Ordine> b)
{
b.HasKey(o => o.Id);
b.Property(o => o.Stato).HasConversion<string>();
b.HasMany(o => o.Righe).WithOne().HasForeignKey("OrdineId");
b.Navigation(o => o.Righe).UsePropertyAccessMode(PropertyAccessMode.Field);
b.ComplexProperty(o => o.Totale);
}
}Oggetti valore: ComplexProperty e conversioni
Da .NET 8 in avanti ComplexProperty è il modo più comodo di mappare un oggetto valore, come Denaro o Indirizzo, su colonne della stessa tabella: Totale_Importo e Totale_Valuta. Non ha identità, non si traccia separatamente e si confronta per valore, come richiede il concetto. Per oggetti valore con una sola proprietà, come Email, basta una conversione di valore (HasConversion) che salva la stringa e ricostruisce l'oggetto in lettura.
Il repository è ancora utile?
Dipende da quello che gli chiedi. Con Entity Framework Core il DbContext è già un'unità di lavoro e un insieme di repository. Un repository per aggregato serve ancora a due cose: esprimere in termini di dominio le ricerche che il dominio fa (OrdiniApertiDelCliente(clienteId)) e nascondere i dettagli di caricamento, per esempio Include sulle righe. Un repository generico con GetAll() e Add() su ogni tabella, invece, aggiunge uno strato senza aggiungere significato. Il confronto fra la libreria più leggera e l'ORM completo lo trovi in Dapper contro Entity Framework.
Le letture non devono passare dal dominio
Per le schermate con elenchi, filtri e totali non serve caricare gli aggregati. Una query diretta sul database, con una proiezione verso un modello di lettura, è più veloce e più semplice. Questa separazione fra scrittura, che passa dal dominio, e lettura, che va dritta ai dati, è l'idea di fondo di CQRS, e ne ho scritto quando conviene e quando no in CQRS: quando conviene davvero.
Come si usano i domain event in .NET per far reagire il sistema?
Un evento di dominio è un oggetto che descrive qualcosa di accaduto nel business, scritto al passato: OrdineConfermato, FidoSuperato. L'aggregato lo registra, e dopo il salvataggio dei dati qualcun altro reagisce, per esempio inviando una conferma o aggiornando il fido.
Registrare l'evento dentro l'aggregato
Quando Conferma() ha successo, l'ordine aggiunge a una lista interna un evento OrdineConfermato(Id, ClienteId, Totale). Non lo invia a nessuno: lo tiene da parte. L'aggregato non sa chi ascolterà, e non deve saperlo, perché il giorno in cui qualcuno aggiunge una notifica via posta l'ordine non cambia.
Pubblicarlo dopo il salvataggio
Il punto in cui gli eventi escono è il salvataggio. Nel metodo SaveChangesAsync sovrascritto del DbContext raccogli gli eventi da tutte le entità tracciate, salvi i dati e solo dopo li passi ai gestori. Se il salvataggio fallisce, gli eventi non partono: nessuna email di conferma per un ordine che non esiste. Per le operazioni che non possono perdersi, come l'invio a un sistema esterno, il modello più sicuro è salvare l'evento nella stessa transazione dell'ordine in una tabella di uscita e farlo spedire da un servizio in background.
Con o senza MediatR
Molti progetti usano MediatR per distribuire gli eventi, ma non è un obbligo. Un'interfaccia IGestoreEvento<T> con i gestori registrati nel contenitore delle dipendenze fa lo stesso lavoro in venti righe, e senza una libreria in più da aggiornare. Il meccanismo di registrazione è lo stesso spiegato in dependency injection in C#: chiedi al contenitore tutti i gestori del tipo di evento e li esegui uno dopo l'altro.
Eventi interni ed eventi verso l'esterno
Un evento di dominio resta dentro l'applicazione e può cambiare forma liberamente. Un evento che esce verso altri sistemi, un messaggio su Azure Service Bus per esempio, è un contratto pubblico e ha un'altra vita: va versionato e non si cambia senza avvisare. Tieni separati i due tipi. Il gestore dell'evento interno OrdineConfermato traduce in un messaggio pubblico con un formato stabile. Se vuoi spingerti oltre e salvare solo gli eventi come fonte di verità, l'approccio ha un nome e un costo, discussi in Event Sourcing in .NET.
Dove va la logica: nell'entità, in un servizio di dominio o in un servizio applicativo?
La regola che riguarda un solo aggregato va nell'aggregato. La regola che coinvolge più aggregati va in un servizio di dominio. Il coordinamento tecnico, come caricare dati e salvare, va in un servizio applicativo. Tenere separati i tre livelli evita il servizio da duemila righe in cui nessuno trova più niente.
Entità: le regole di un solo oggetto
Se la regola si esprime guardando solo i dati dell'ordine, il posto è l'ordine. "Un ordine confermato non si modifica" e "il totale è la somma delle righe meno lo sconto" stanno dentro la classe. Quando esiti, chiediti se per verificare la regola hai bisogno di qualcosa che l'ordine non possiede: se no, è sua.
Servizio di dominio: le regole fra più oggetti
"Un cliente non può confermare un ordine oltre il fido" coinvolge l'ordine e il cliente, che sono aggregati diversi. Un servizio di dominio come VerificaFido riceve entrambi, applica la regola e restituisce l'esito. Non ha dipendenze tecniche: niente database, niente HTTP. È una classe che si prova con due oggetti in memoria. Nome e contenuto parlano il linguaggio del business, esattamente come le entità.
Servizio applicativo: il regista senza regole
Il servizio applicativo, o il gestore del comando, fa le cose noiose: carica l'ordine dal repository, carica il cliente, chiama il servizio di dominio, invoca ordine.Conferma(), salva, pubblica gli eventi. Se dentro compare un if sul business, probabilmente la regola è nel posto sbagliato. Un buon servizio applicativo si legge come una ricetta e non contiene nessuna decisione che l'azienda possa voler cambiare.
Il segnale del modello anemico
Se tutte le entità hanno solo proprietà e tutta la logica sta in classi chiamate OrdineService, ClienteManager, OrdineHelper, hai un modello anemico: oggetti che trasportano dati e servizi che li modificano. Funziona finché il progetto è piccolo. Quando le regole crescono, ogni servizio può violare gli invarianti di qualsiasi oggetto, e il disordine di Elena ritorna. La correzione è graduale: sposta una regola alla volta dal servizio all'entità, chiudi il setter pubblico che lasciava scavalcare il metodo e osserva quanti punti del codice smettono di compilare. Quei punti erano i modi in cui il sistema poteva rompersi.
Questo è il tipo di ragionamento su cui costruiamo il lavoro nel percorso per architetti: dove mettere una regola, come accorgersi che sta nel posto sbagliato, come spostarla senza fermare il progetto. Per il quadro più ampio dei livelli in cui questi servizi si inseriscono, vedi Clean Architecture in C#.
Come si testa un modello di dominio senza database?
Un modello di dominio ben scritto si prova con test unitari puri: crei gli oggetti in memoria, chiami i metodi e verifichi lo stato o l'eccezione, senza database, senza contenitore e senza simulazioni. È il vantaggio più immediato del DDD e il motivo per cui il test di Giacomo, nell'articolo sull'iniezione delle dipendenze, girava in pochi millisecondi.
Un test per regola, con il nome della regola
Ogni regola del business diventa un test, e il nome del test è la regola scritta in italiano: Un_ordine_senza_righe_non_si_conferma, Un_ordine_confermato_non_accetta_nuove_righe. Quando un test si rompe fra sei mesi, il nome ti dice quale regola è stata violata, senza aprire il codice. Con xUnit e FluentAssertions il test è di tre righe: prepari l'ordine, chiami il metodo, verifichi che l'eccezione abbia il messaggio atteso.
Costruttori di test per non ripetersi
Se ogni test ricostruisce un ordine con cliente, tre righe e uno sconto, il file diventa illeggibile. Una piccola classe OrdineBuilder con metodi come ConRighe(3) e Confermato() ti dà un ordine pronto in una riga e rende evidente che cosa cambia da un test all'altro. Resta nel progetto dei test, non nel dominio.
I test sugli eventi
Verificare che Conferma() registri l'evento OrdineConfermato con i dati giusti è un test banale quando l'evento sta in una lista dell'aggregato. Non hai bisogno di un gestore, di un bus o di un database: guardi la lista. I gestori si provano a parte, ciascuno con i propri dati in ingresso.
Il test di integrazione per la mappatura
Una cosa il test unitario non vede: se Entity Framework riesce davvero a salvare e rileggere l'aggregato. Per questo serve un piccolo gruppo di test di integrazione con un database vero, di solito SQL Server in un contenitore Docker o SQLite in memoria per i casi semplici. Salvi un ordine con tre righe, lo rileggi con un nuovo DbContext e confronti. Bastano pochi test per aggregato, e cambiano raramente. Sono quelli che ti salvano il giorno in cui aggiorni la versione di Entity Framework Core e una conversione di valore smette di funzionare.
Il rapporto tipico che vedo nei progetti sani è di cento test unitari sul dominio per ogni cinque di integrazione. Se il tuo rapporto è invertito, il dominio sta chiedendo al database di fare cose che dovrebbe saper fare da solo.
Che cos'è un contesto delimitato e come si organizza una soluzione .NET?
Un contesto delimitato è un confine dentro cui un termine ha un solo significato: "cliente" per le vendite non è "cliente" per l'amministrazione, e ciascun contesto ha il proprio modello. In una soluzione .NET si traduce in progetti separati, ognuno con il proprio dominio, e in regole precise su chi può parlare con chi.
Lo stesso nome, due significati
Nell'azienda di Elena il "cliente" delle vendite ha un listino, un agente e una zona. Il "cliente" dell'amministrazione ha una partita IVA, un termine di pagamento e un fido. Tenerli nella stessa classe significa che ogni modifica dell'ufficio vendite può rompere l'ufficio contabilità. Con due contesti, Vendite e Amministrazione, ciascuno ha la sua classe Cliente e i due si parlano tramite l'identificatore e qualche evento. È una duplicazione voluta, e fa risparmiare molto lavoro di coordinamento.
La struttura dei progetti
In una soluzione a strati, ogni contesto ha di solito quattro progetti: Dominio (classi pure, nessuna dipendenza), Applicazione (casi d'uso, dipende dal dominio), Infrastruttura (Entity Framework, posta, file, dipende da dominio e applicazione) e il punto di ingresso, un'API o un sito, che li assembla. Il dominio non referenzia niente: se apri il file di progetto e trovi un pacchetto NuGet di accesso ai dati, qualcosa è fuori posto. Il disegno complessivo dei livelli è quello di Clean Architecture in C#.
Monolite modulare prima dei servizi separati
Dividere in contesti non significa dividere in microservizi. Nella grande maggioranza dei gestionali che incontro, la scelta giusta è un unico processo con più moduli, ognuno con il proprio dominio e il proprio schema nel database, che comunicano tramite interfacce pubbliche ed eventi interni. Se un giorno un modulo deve diventare un servizio a sé, il confine esiste già. Il confronto fra le due strade è in monolite o microservizi.
Come si trova il confine
Le tabelle non te lo mostrano, i termini sì. Quando due reparti usano la stessa parola con significati diversi, o quando una modifica richiesta da un reparto fa tremare un altro, hai trovato una frontiera. Una sessione di due ore con un lavagna e con una persona di ciascun reparto produce, di solito, una mappa dei contesti più affidabile di una settimana di analisi del database.
Quali errori di DDD si vedono più spesso nei progetti reali?
Gli errori più comuni sono tre: applicare il DDD a un problema che non lo richiede, costruire aggregati troppo grandi e usare i nomi del DDD senza spostare le regole nelle classi. Tutti e tre producono lo stesso risultato: più codice da scrivere e nessun beneficio.
Il DDD come cerimonia
Succede quando un team trasforma un gestionale con quattro tabelle in un progetto con dodici cartelle, una fabbrica per ogni classe, tre livelli di interfacce e un repository per ogni tabella. Le regole da proteggere sono due, ma la struttura è per venti. Il costo lo paga chi arriva dopo e deve capire dove sia finita una riga di codice che prima stava in un file. La regola di partenza è sempre la stessa: aggiungi un pezzo di struttura quando senti il dolore che risolve, non prima.
L'aggregato gigante
È l'ordine che contiene il cliente, i prodotti, le spedizioni e le fatture. Ogni operazione carica centinaia di oggetti, due utenti che lavorano su parti diverse si bloccano a vicenda e la concorrenza ottimistica di Entity Framework lancia eccezioni a ripetizione. Il rimedio è quello visto nella sezione sugli aggregati: dividere in aggregati più piccoli collegati per identificatore e accettare la coerenza finale dove il business la tollera.
Le proprietà pubbliche con setter
Un'entità con public decimal Totale { get; set; } non protegge niente, anche se si trova in una cartella chiamata Domain. Il test pratico: cerca nel progetto quante volte si scrive direttamente ordine.Stato = ... fuori dalla classe. Se il numero è maggiore di zero, il modello è anemico e le regole sono distribuite. Si risolve un setter alla volta, in modo che il compilatore ti mostri ogni punto da correggere.
Le eccezioni usate come flusso normale
Lanciare un'eccezione di dominio per una regola violata è giusto, perché segnala un errore di programmazione o un dato non valido. Usarla per controllare un caso atteso, come "il cliente ha superato il fido", costa prestazioni e rende il codice difficile da seguire. Per i casi previsti dal business restituisci un risultato esplicito (un tipo Esito con successo o errore) e riserva le eccezioni agli stati impossibili.
Parlare di DDD senza parlare con il business
L'errore più subdolo: gli sviluppatori inventano il vocabolario da soli. Il risultato sembra un modello di dominio e invece è un modello degli sviluppatori. Se nessuno dell'azienda riconosce i nomi delle classi, il linguaggio condiviso non esiste e il DDD è rimasto sulla carta. Una verifica semplice: fai leggere l'elenco dei metodi di Ordine al commerciale. Se capisce che cosa fa ognuno, sei sulla strada giusta.
Come si introduce il DDD in un progetto .NET che esiste già?
Si parte da una regola sola, la più costosa da sbagliare, e la si sposta in un'entità. Si chiude il setter pubblico, si lascia che il compilatore indichi i punti da correggere e si ripete con la regola successiva. Nessuna riscrittura totale, nessun blocco del progetto.
Scegli la regola che fa più male
Elena avrebbe cominciato dalla regola "un ordine confermato non si modifica", perché è quella che ha causato la fattura sbagliata. Chiedi al business quale regola, se violata, costa più denaro o più tempo di assistenza, e parti da lì. Una vittoria visibile nelle prime due settimane convince il team molto più di un piano trimestrale.
Il metodo in cinque passi
- Cerca tutti i punti del codice che scrivono la proprietà in questione.
- Crea nell'entità il metodo con il nome del business (
Conferma). - Sposta nel metodo i controlli che oggi sono sparsi.
- Rendi privato il setter della proprietà.
- Correggi gli errori di compilazione chiamando il nuovo metodo.
Per un'entità media il lavoro richiede da mezza giornata a due giorni, e al termine hai una regola in un posto solo e un test che la protegge. La tecnica è la stessa della modernizzazione per piccoli passi che descrivo in Strangler Fig Pattern: il vecchio e il nuovo convivono, e il nuovo cresce ai bordi.
Cresci per aree, non per strati
Una volta che l'ordine è in ordine, passa a un'altra area che fa male, non agli altri strati dello stesso modulo. L'obiettivo è che le zone ben modellate si ampliano finché l'intero gestionale ne è coperto, e non c'è fretta: un'area modellata bene a trimestre vale più di un progetto "DDD" lasciato a metà.
L'intelligenza artificiale aiuta, ma non decide i confini
Gli assistenti di programmazione sono utili per generare un oggetto valore con la sua validazione, o per scrivere la configurazione di Entity Framework di un aggregato. Non sanno invece che cosa significa "ordine confermato" nella tua azienda, e proporranno confini plausibili e sbagliati. Le domande sul dominio restano al team e al business, la scrittura meccanica va all'assistente, e ogni riga generata passa da una revisione come le altre.
Elena ha cominciato dalla regola che le aveva rovinato un martedì. Ha spostato il controllo dentro Ordine.Conferma(), ha reso privato il setter di Stato e ha lasciato che il compilatore le mostrasse i sette punti che scrivevano lo stato dall'esterno. Ne ha corretti sei in una mattina. Il settimo era l'importazione notturna, quella che nessuno ricordava. Il martedì dopo il commerciale ha cambiato la regola del fido, e Elena ha toccato un metodo.
Se vuoi arrivare a progettare i confini del tuo gestionale con la tua testa, riconoscere un aggregato troppo grande e scegliere dove mettere ogni regola, nel corso Architetto Software con AI di Sviluppatore Migliore lavoriamo su casi reali con il mio feedback diretto, in lezioni che si incastrano con il lavoro. Porta la classe con più setter pubblici che hai: la mettiamo in sicurezza insieme.
Domande frequenti
Si scrivono entità con setter privati e metodi che portano i nomi del business, oggetti valore con i record, aggregati con una radice e un confine chiaro. Entity Framework Core mappa il tutto con configurazioni separate, così il dominio non dipende dall'accesso ai dati.
Conviene quando il valore ha regole proprie, come un importo con valuta o una partita IVA. Un oggetto valore valido per costruzione toglie i controlli dal resto del codice e impedisce errori come sommare euro e dollari.
Un aggregato è un gruppo di oggetti modificati come unità, con una radice che fa rispettare le regole. Deve essere piccolo: dentro stanno solo gli oggetti che devono essere coerenti nello stesso istante, gli altri si collegano per identificatore.
Serve un repository per aggregato se vuoi esprimere le ricerche in termini di dominio e nascondere i dettagli di caricamento. Un repository generico su ogni tabella aggiunge uno strato senza significato, perché il DbContext è già un'unità di lavoro.
Con test unitari puri: crei gli oggetti in memoria, chiami i metodi e verifichi stato o eccezione, senza database. Pochi test di integrazione con un database vero controllano solo che la mappatura di Entity Framework salvi e rilegga l'aggregato.
Si parte dalla regola più costosa da sbagliare, la si sposta in un metodo dell'entità, si rende privato il setter e si correggono gli errori di compilazione. Si ripete per aree, senza riscrivere tutto e senza fermare il progetto.
