Microservizi o monolite: come si sceglie?
Il segnale più affidabile è la dimensione e struttura del team: team piccoli e singoli con monolite, team multipli con necessità di rilascio indipendente con microservizi. Il secondo segnale è la differenza reale nei requisiti di scalabilità tra le componenti: se tutto scala insieme, non serve scomporre.
La soluzione più spesso sottovalutata è il Modular Monolith: i vantaggi della separazione del codice senza il costo operativo dei microservizi. Funziona per la maggioranza dei sistemi che non sono Amazon o Netflix.

Quasi ogni discussione architetturale prima o poi arriva qui: microservizi o monolite? Ed è quasi sempre seguita dalla risposta più inutile che esiste nel mondo dell'architettura software: "dipende". "Dipende" non è una risposta. È una scusa per non prendere una posizione.
Questo articolo prende una posizione e fornisce un framework decisionale concreto basato su segnali reali, non su hype tecnologico o su "è quello che fanno Netflix e Amazon". Perché Netflix ha 200 milioni di utenti e migliaia di ingegneri. Il tuo progetto probabilmente no.
Il punto di partenza è una premessa scomoda: la maggioranza dei progetti che hanno adottato i microservizi negli ultimi cinque anni non ne aveva bisogno. Molti stanno tornando al monolite o a varianti più moderate. Amazon stessa, che ha reso famosi i microservizi, ha pubblicato nel 2023 un post-mortem tecnico dove i Prime Video engineers hanno consolidato un sistema di microservizi in un monolite, riducendo i costi del 90%.
Non è un caso isolato. È il sintomo di un problema sistemico: l'architettura software viene scelta in base al marketing e alle conference talk, non in base alle esigenze reali del progetto. Questa guida ti aiuta a fare esattamente il contrario: partire dal tuo contesto, non dall'ideale astratto.
Se stai leggendo questo articolo, probabilmente ti trovi davanti a una decisione concreta: stai avviando un nuovo progetto, o stai valutando se il sistema attuale ha bisogno di cambiare. In entrambi i casi, quello che conta non è la tecnologia in sé, ma capire quali segnali guardare e quali domande fare prima di scegliere.
Il mito dei microservizi: perché il marketing ha distorto la realtà architetturale
I microservizi sono diventati popolari intorno al 2014-2015, trainati da conference talk di Netflix, Amazon e Spotify su come avevano reso i loro sistemi scalabili. Il messaggio arrivato alla maggior parte del settore era semplice: i microservizi sono il futuro, il monolite è il passato.
Il problema è che quel messaggio ha completamente perso il contesto originale. Netflix non ha scelto i microservizi perché sono "meglio". Li ha scelti perché aveva un problema specifico: decine di team indipendenti che dovevano rilasciare funzionalità nuove ogni giorno senza bloccarsi a vicenda, su una piattaforma con centinaia di milioni di utenti e requisiti di availability estremi. Il monolite, in quel contesto specifico, non reggeva più.
Il 99% delle aziende italiane che sviluppa software non ha quel problema. Ha 3 developer che lavorano su un gestionale, o 10 persone che costruiscono una piattaforma SaaS per il mercato verticale. Applicare l'architettura di Netflix a questi contesti non è "fare le cose per bene". È over-engineering puro che aumenta la complessità senza nessun beneficio reale.
Martin Fowler, uno dei teorici che ha contribuito a formalizzare i microservizi, ha scritto esplicitamente che il consiglio di default dovrebbe essere di non usarli. Il punto di partenza dovrebbe essere il monolite, da cui estrarre servizi solo quando i segnali concreti lo giustificano. Questo non lo dice chi "non capisce" i microservizi. Lo dice chi li capisce profondamente.
Un altro dato che viene raramente citato: Sam Newman, autore del libro di riferimento sui microservizi, ha dichiarato in più occasioni che molti team adottano i microservizi senza avere le basi organizzative e tecniche per gestirli correttamente. Il risultato è quello che lui chiama "distributed monolith": tutti i problemi dei microservizi senza nessuno dei benefici.
Il mercato italiano ha un problema specifico in questo senso: molte software house adottano architetture "enterprise" che hanno visto alle conferenze o letto nei blog internazionali, senza valutare se hanno senso per il volume di traffico, la dimensione del team e le competenze disponibili localmente. Questo porta a sistemi inutilmente complessi, difficili da mantenere, e costosissimi da operare.
Cos'è un monolite nel 2026: definizioni precise e tipi diversi di monolite
Prima di confrontare le due architetture, è fondamentale allinearsi sulle definizioni. "Monolite" nel 2026 non significa necessariamente "codice spaghetti scritto male". Esistono tipi diversi di monolite con caratteristiche molto diverse tra loro.
Il monolite tradizionale (single deployable unit)
Il monolite tradizionale è un singolo artefatto deployabile che contiene tutta la logica applicativa. Un progetto ASP.NET Core con tutti i controller, tutti i servizi, tutta la logica di business in un unico assembly che gira su un singolo processo. Il database è condiviso da tutto il sistema.
Questo è il tipo di monolite che la narrativa dominante attacca. Ma il problema non è il monolite in sé: è il monolite mal strutturato, dove tutto chiama tutto senza confini chiari. Quel tipo di monolite viene spesso chiamato "big ball of mud" e rappresenta un problema di disciplina architetturale, non di scelta tecnologica.
Il Modular Monolith
Il Modular Monolith è ancora un singolo artefatto deployabile, ma internamente organizzato in moduli con confini espliciti. Ogni modulo ha la propria area del database, la propria API pubblica verso gli altri moduli, e confini di dipendenze controllati. I moduli non si chiamano liberamente: comunicano attraverso contratti definiti, esattamente come farebbero i microservizi, ma senza l'overhead di rete.
Questo è il tipo di monolite che andrebbe quasi sempre considerato prima di decidere se servono i microservizi. Rende la codebase mantenibile, le responsabilità chiare, e i confini abbastanza definiti da permettere eventualmente l'estrazione di servizi in futuro.
Il monolite distribuito (da evitare)
Il monolite distribuito è la situazione che si ottiene quando si cerca di fare microservizi senza avere le condizioni giuste. Il sistema è fisicamente distribuito su più servizi, ma questi servizi sono fortemente accoppiati: condividono il database, si chiamano sincronamente in cascata, devono essere deployati insieme. Ha tutti i problemi del monolite più tutta la complessità operativa dei microservizi. È il risultato peggiore possibile e purtroppo molto comune.
Il monolite distribuito non è microservizi mal implementati. È un'architettura separata, genuinamente peggiore del monolite tradizionale per la maggior parte degli scopi.
Prima di decidere tra microservizi e monolite, è utile capire quali sono i vantaggi reali di ognuno. Il monolite ha vantaggi concreti e spesso sottovalutati: zero overhead di rete nella comunicazione interna, transazioni ACID semplici, debugging lineare, deployment con un singolo artefatto e pipeline, e un costo operativo di gran lunga inferiore. Un sistema monolitico ben scritto può gestire milioni di richieste al giorno senza problemi. Stack Overflow, uno dei siti più trafficati del mondo per i developer, ha gestito miliardi di richieste al mese con un monolite per anni.
Cos'è un microservizio: definizione corretta e confini del dominio
Un microservizio è un servizio autonomo responsabile di un bounded context specifico del dominio, che può essere deployato, scalato e sviluppato indipendentemente dagli altri servizi del sistema. La parola chiave non è "micro" intesa come "piccolo": è indipendente.
Un servizio è un vero microservizio se soddisfa questi tre criteri:
- Deploy indipendente: può essere rilasciato in produzione senza coordinamento con altri team o altri servizi. Se per fare il deploy di un servizio devi avvisare altri 3 team e aspettare che tutti siano pronti, non hai microservizi.
- Scalabilità indipendente: può essere scalato orizzontalmente in modo autonomo quando riceve picchi di carico, senza dover scalare tutto il resto del sistema.
- Sviluppo indipendente: un team separato può lavorarci senza dipendere dagli altri team, con la possibilità di usare stack tecnologici diversi se necessario.
Il confine di un microservizio dovrebbe corrispondere a un bounded context del dominio secondo i principi del Domain-Driven Design. Non si tratta di tagliare il sistema per funzionalità tecniche (database, API, presentazione), ma per concetti del dominio: Ordini, Inventario, Pagamenti, Notifiche. Ogni bounded context ha il proprio linguaggio, i propri dati, le proprie regole di business.
La dimensione del microservizio è irrilevante. Un microservizio può avere 200 righe di codice o 20.000. Quello che conta è che rispetti il principio di responsabilità singola a livello di dominio e che i suoi confini siano abbastanza stabili da permettere sviluppo indipendente nel tempo.
Un errore comune è creare microservizi troppo piccoli: servizi che si occupano di singole entità del database invece che di bounded context completi. Questo genera quella che viene chiamata "nanoservice architecture", dove le chiamate di rete tra servizi diventano così frequenti da superare di gran lunga qualsiasi beneficio dell'indipendenza. Il risultato è performance disastrosa e una complessità gestionale enorme per qualsiasi operazione business.
Per approfondire come i bounded context si traducono in architettura concreta, vale la pena leggere l'articolo sui pattern architetturali software, che copre anche DDD e la separazione delle responsabilità a livello di design.
I 5 segnali concreti che indicano quando passare ai microservizi
I microservizi non si scelgono per motivazioni filosofiche o perché "è quello che fanno le aziende tech moderne". Si scelgono quando esistono segnali concreti nel tuo progetto che indicano un problema reale che solo i microservizi risolvono meglio del monolite.
Segnale 1: team multipli che si bloccano a vicenda costantemente
Se hai 4 team che lavorano sullo stesso codebase e si intralciano continuamente, con conflitti di merge quotidiani, deployment bloccati perché un team non è pronto, e riunioni continue per coordinare le release, stai pagando il costo organizzativo del monolite senza il beneficio della sua semplicità. Il principio di Conway dice che l'architettura del software tende a rispecchiare la struttura organizzativa. Se la tua organizzazione è distribuita, la tua architettura dovrebbe esserlo.
Segnale 2: requisiti di scalabilità radicalmente diversi tra componenti
Se il componente di ricerca riceve 500 richieste al secondo mentre l'area amministrativa ne riceve 2, e queste differenze sono stabili e prevedibili, estrarre il motore di ricerca come servizio separato ha senso economico: puoi scalarlo indipendentemente senza dover scalare tutto il resto. Ma attenzione: questo segnale è valido solo quando i requisiti di scalabilità sono davvero radicalmente diversi e confermati dalla produzione, non ipotetica.
Segnale 3: tecnologie incompatibili necessarie per componenti diversi
Se hai bisogno di Python con TensorFlow per il componente di machine learning, di .NET per il backend principale, e magari di Go per un componente ad alta concorrenza, i microservizi sono la risposta naturale. All'interno di un monolite .NET non puoi integrare un modulo Python in modo nativo. I confini di servizio permettono la libertà tecnologica dove serve davvero.
Segnale 4: isolamento regolatorio o di compliance
In settori come finanza, healthcare o telecomunicazioni, alcune funzionalità devono essere fisicamente isolate per motivi di compliance. Il sistema di pagamenti deve essere certificato PCI-DSS separatamente dal resto. I dati clinici devono essere isolati in base a normative GDPR specifiche. In questi casi, i microservizi non sono una scelta architetturale opzionale, ma un requisito regolatorio concreto.
Segnale 5: resilienza differenziata con failure isolation
Se un componente deve continuare a funzionare anche quando altri componenti del sistema sono down, i microservizi permettono di isolare i failure. In un monolite, se il processo crasha per un bug in un modulo, tutto il sistema va offline. Con i microservizi, un servizio che crasha non abbatte gli altri. Questa proprietà è particolarmente rilevante per sistemi con SLA elevati su parti specifiche del sistema.
I 5 segnali che indicano che il monolite è la scelta giusta (e lo resterà)
Esistono segnali altrettanto concreti che indicano che il monolite non solo va bene oggi, ma continuerà ad andare bene nel futuro prevedibile del tuo progetto. Riconoscerli ti salva da anni di complessità operativa inutile.
Segnale 1: team piccolo o singolo team
Sotto i 10-15 developer che lavorano allo stesso sistema, i microservizi aggiungono complessità senza benefici organizzativi. L'overhead di coordinamento tra team che giustifica i microservizi non esiste. Un singolo team con un monolite ben strutturato può muoversi molto più velocemente di un team che deve gestire pipeline separate, API versioning e service discovery per ogni modulo.
Segnale 2: dominio ancora in evoluzione e non stabilizzato
Se stai costruendo un nuovo prodotto e il dominio non è ancora chiaro, dividere in microservizi presto è pericoloso. I bounded context cambieranno con la comprensione del dominio. Rinegoziare i confini tra microservizi in produzione è doloroso e costoso. Il monolite permette di rifattorizzare i confini a costo quasi nullo finché il codice non è distribuito.
Segnale 3: team senza esperienza operativa distribuita
Gestire un sistema a microservizi richiede competenze specifiche: Kubernetes o un orchestratore equivalente, service mesh come Istio o Linkerd, distributed tracing con Jaeger o Zipkin, gestione delle failure parziali con circuit breaker, configurazione distribuita. Se il team non ha questa esperienza, il costo di acquisirla nel contesto di un sistema in produzione è enorme. Iniziare con il monolite e acquisire queste competenze gradualmente è più sicuro.
Segnale 4: budget operativo limitato
I microservizi costano di più da operare. Più pipeline CI/CD, più ambienti di staging, più risorse di computing per i container, più costi di rete tra servizi. Per una startup o una software house italiana di medie dimensioni, questi costi non sono trascurabili. Se il budget è un vincolo reale, il monolite è più efficiente dal punto di vista economico.
Segnale 5: sistema con requisiti di transazionalità forte
Se il dominio richiede transazioni distribuite frequenti, come nel caso di sistemi ERP, contabilità o processi di approvazione multi-step, il monolite semplifica enormemente la gestione della consistenza. Le transazioni ACID all'interno di un singolo processo sono banali. In un sistema distribuito richiedono Saga pattern, compensating transactions e gestione dell'eventual consistency, con una complessità di codice e testing che cresce in modo non lineare.
Il Modular Monolith: la soluzione che il 70% dei team dovrebbe scegliere
Tra il monolite tradizionale e i microservizi esiste uno spazio intermedio che risolve la maggior parte dei problemi reali con una frazione della complessità: il Modular Monolith. È la soluzione che il 70% dei team dovrebbe scegliere ma che spesso viene ignorata perché non fa notizia alle conferenze.
Un Modular Monolith è un singolo artefatto deployabile diviso internamente in moduli con confini espliciti. Ogni modulo ha:
- La propria area del database (schema separato, non database separato)
- La propria API pubblica verso gli altri moduli, tipicamente definita come interfacce C# o eventi interni
- Le proprie dipendenze interne, non condivise con altri moduli
- Confini che impediscono l'accesso diretto alle entità interne degli altri moduli
Come si implementa un Modular Monolith in .NET
In un progetto .NET, il Modular Monolith si implementa tipicamente con una struttura a progetti separati nella stessa solution. Ogni modulo è un progetto con le proprie classi di dominio, la propria infrastruttura, e un progetto di contratti pubblici che espone solo quello che gli altri moduli possono usare.
La comunicazione tra moduli può avvenire in due modi: attraverso chiamate dirette alle interfacce pubbliche (per operazioni sincrone), oppure attraverso un event bus in-process (per operazioni asincrone e per ridurre l'accoppiamento). Le librerie come MediatR permettono di implementare un event bus interno con overhead minimo.
Un pattern comune è definire per ogni modulo un file di registrazione delle dipendenze, tipicamente un metodo di estensione su IServiceCollection, che registra tutti i servizi interni del modulo senza esporli. Solo le interfacce pubbliche sono visibili agli altri moduli. Questo crea una separazione netta simile a quella dei microservizi, ma all'interno dello stesso processo.
I vantaggi del Modular Monolith rispetto ai microservizi
Il Modular Monolith mantiene tutti i vantaggi operativi del monolite: un singolo artefatto da deployare, transazioni ACID semplici, debugging lineare, nessun overhead di rete. Allo stesso tempo, fornisce la separazione delle responsabilità, confini espliciti tra le aree del sistema, e la possibilità di estrarre moduli in servizi separati in futuro quando le condizioni lo giustificano.
Il secondo vantaggio è strategico: se un giorno un modulo cresce abbastanza da giustificare l'estrazione come microservizio, i confini sono già definiti. La migrazione è chirurgica invece di essere una riscrittura. Stai essenzialmente costruendo un sistema che può evolvere verso i microservizi solo dove necessario, senza dover affrontare tutto in una volta.
Il Modular Monolith non è una soluzione temporanea. È un'architettura pienamente legittima che può reggere sistemi complessi per anni, e che è diventata la scelta default consigliata da molti degli architetti software più autorevoli del settore.
I costi nascosti dei microservizi che nessuno calcola prima di partire
Il costo dei microservizi viene quasi sempre sottostimato. Le slide delle conferenze mostrano i benefici: scalabilità, indipendenza dei team, resilienza. Non mostrano mai il conto operativo reale. Prima di scegliere i microservizi, questi costi vanno messi esplicitamente in bilancio.
Infrastruttura e orchestrazione
I microservizi richiedono un sistema di orchestrazione dei container, tipicamente Kubernetes. Kubernetes è uno strumento potente ma complesso: richiede competenze specifiche per setup, configurazione, monitoraggio e manutenzione. Per un team di 5 persone, avere almeno una persona che gestisce il cluster Kubernetes è un costo fisso. Su cloud managed (AKS, EKS, GKE), il costo economico del cluster è aggiuntivo rispetto ai costi del computing stesso.
Osservabilità e distributed tracing
In un monolite, un log file e un debugger bastano per la maggior parte delle investigazioni. In un sistema a microservizi, tracciare una richiesta che attraversa 5 servizi richiede distributed tracing (OpenTelemetry, Jaeger, Zipkin), correlazione di log tra servizi diversi, e metriche per ogni servizio. Stack come Prometheus con Grafana, o soluzioni SaaS come Datadog, aggiungono complessità tecnica e costi economici non trascurabili.
Service mesh e comunicazione sicura
Per gestire la comunicazione tra servizi in modo sicuro e affidabile, molti team a microservizi finiscono per adottare un service mesh come Istio o Linkerd. Questi strumenti gestiscono mTLS tra servizi, load balancing avanzato, circuit breaking e retry policies a livello di infrastruttura. Ma hanno una curva di apprendimento significativa e aggiungono overhead ai deployment.
Testing di integrazione e contract testing
Nel monolite, i test di integrazione testano componenti dello stesso processo. Nei microservizi, i test di integrazione devono gestire servizi separati, con l'opzione di mock, stub o service virtualization. Il contract testing (con strumenti come Pact) diventa necessario per garantire che i contratti tra servizi siano rispettati quando i team cambiano le API. Questo aggiunge una intera disciplina di testing che nel monolite non esiste.
Gestione delle failure parziali
In un sistema distribuito, una chiamata a un servizio può fallire per mille motivi: timeout di rete, servizio temporaneamente non disponibile, sovraccarico. Gestire queste failure parziali richiede pattern come circuit breaker, retry con exponential backoff, bulkhead, e la progettazione esplicita di ogni interazione tra servizi in modo fault-tolerant. Questa complessità non esiste nel monolite.
Studi empirici indicano che il costo operativo di un sistema a microservizi è 3-5 volte quello di un monolite equivalente. Se il tuo progetto non ha i segnali che giustificano questo costo aggiuntivo, stai pagando un prezzo altissimo per complessità che non ti serve.
Per una visione più ampia delle scelte architetturali e dei loro impatti, l'articolo su Clean Architecture in C# fornisce un contesto utile su come organizzare il codice indipendentemente dalla scelta tra monolite e microservizi.
Pattern Strangler Fig: come migrare da monolite a microservizi senza riscrivere tutto
Se sei nella situazione in cui hai un monolite esistente e hai i segnali concreti che giustificano l'estrazione di microservizi, il pattern Strangler Fig è il modo più sicuro per farlo senza riscrivere il sistema dall'inizio.
Il nome viene da un tipo di fico tropicale (Ficus aurea) che cresce avvolgendo un albero ospite, lentamente sostituendo la struttura dell'albero originale fino a quando l'albero ospite muore e rimane solo il fico. Il pattern software funziona in modo analogo: nuovi servizi crescono "intorno" al monolite, intercettando traffico, finché il monolite originale non è stato gradualmente rimpiazzato.
Come funziona il pattern Strangler Fig
L'implementazione pratica del pattern avviene in quattro fasi:
Fase 1: identifica il bounded context da estrarre. Non estrarre funzionalità a caso. Parti dal bounded context che ha i segnali più forti per l'estrazione: quello con i requisiti di scalabilità più diversi, o quello che causa più conflitti di merge tra team, o quello con i requisiti di compliance specifici.
Fase 2: crea il nuovo servizio. Costruisci il nuovo servizio parallelamente al monolite, con la propria infrastruttura, il proprio database, la propria pipeline CI/CD. Non sostituire il codice nel monolite: duplicalo nel servizio finché il servizio non è pronto per il traffico di produzione.
Fase 3: inserisci un facade/proxy. Aggiungi un livello di routing (tipicamente un API Gateway o un proxy configurabile come YARP in .NET) davanti al sistema. Questo layer decide se instradare le richieste al monolite o al nuovo servizio, permettendo un rollout graduale e la possibilità di fare rollback immediato.
Fase 4: migra il traffico progressivamente. Inizia con il 1-5% del traffico verso il nuovo servizio. Monitora attentamente. Se tutto va bene, aumenta progressivamente. Solo quando il servizio ha gestito il 100% del traffico in produzione per un periodo sufficientemente lungo, rimuovi il codice corrispondente dal monolite.
Le condizioni necessarie per il successo della migrazione
Il pattern Strangler Fig funziona bene solo se il monolite ha confini sufficientemente chiari per identificare cosa estrarre. Se il monolite è un big ball of mud dove tutto dipende da tutto, la migrazione è quasi impossibile senza una fase preliminare di refactoring interno.
Per questo motivo, investire in un Modular Monolith fin dall'inizio ha un valore strategico: stai costruendo confini che potranno essere usati come linee di estrazione se e quando diventa necessario. I confini di un modulo nel Modular Monolith corrispondono quasi direttamente ai confini di un microservizio.
Service mesh, API gateway, service discovery: l'infrastruttura che i microservizi richiedono
Uno degli aspetti più sottovalutati dei microservizi è l'infrastruttura necessaria per farli funzionare correttamente in produzione. Ogni componente di questa infrastruttura risolve problemi reali, ma aggiunge complessità e richiede competenze specifiche.
API Gateway
In un sistema a microservizi, i client esterni (frontend, app mobile, integrazioni terze parti) non dovrebbero mai chiamare direttamente i servizi interni. Un API Gateway è il punto di ingresso unico che gestisce: routing delle richieste ai servizi appropriati, autenticazione e autorizzazione, rate limiting, trasformazione delle richieste, aggregazione di risposte da più servizi.
In ambito .NET e Azure, le opzioni più comuni sono Azure API Management, Ocelot (open source .NET), e YARP (Yet Another Reverse Proxy, sviluppato da Microsoft). Ognuna ha un profilo diverso per complessità e funzionalità.
Service Discovery
In un sistema dinamico dove i container si scalano e si spostano, i servizi non possono comunicare tra loro con indirizzi IP fissi. Il service discovery è il meccanismo che permette ai servizi di trovarsi: quando un servizio si avvia, si registra in un registry centrale (Consul, etcd, o il servizio DNS interno di Kubernetes). Quando un altro servizio deve comunicarci, risolve l'indirizzo attuale attraverso il registry.
Service Mesh
Un service mesh è un layer di infrastruttura che gestisce tutta la comunicazione tra servizi: mTLS (mutual TLS) per la comunicazione sicura, load balancing tra istanze, circuit breaking, retry logic, e osservabilità della comunicazione. Le opzioni più note sono Istio e Linkerd. Aggiungono overhead ma spostano la responsabilità della resilienza dall'applicazione all'infrastruttura.
La domanda da farsi è: il tuo team ha le competenze per gestire tutto questo? E hai davvero bisogno di tutto questo per il volume di traffico e la complessità del tuo sistema? In molti casi, la risposta onesta è no.
Casi studio: quando i microservizi hanno salvato il progetto e quando lo hanno affondato
I casi studio concreti sono più informativi di qualsiasi principio astratto. Questi due scenari sono rappresentativi di pattern che si ripetono frequentemente nel mercato italiano.
Caso 1: la piattaforma e-commerce che aveva bisogno dei microservizi
Una piattaforma e-commerce italiana di fascia media ha raggiunto un punto critico: il team di sviluppo era cresciuto a 25 persone, divise in 4 team funzionali (catalogo prodotti, ordini, pagamenti, logistica). Il monolite originale, ben strutturato, era diventato un collo di bottiglia organizzativo: ogni release richiedeva coordinamento tra tutti e 4 i team, i conflitti di merge erano frequenti, e il checkout in periodi di picco (Black Friday) doveva reggere 50x il traffico normale mentre il catalogo reggeva tranquillamente.
La decisione di estrarre prima il servizio di checkout come microservizio separato, poi il motore di ricerca del catalogo, ha permesso a queste due funzionalità di scalare indipendentemente durante i picchi. Il resto del sistema è rimasto come Modular Monolith. Il risultato è un'architettura ibrida dove i microservizi esistono solo dove i segnali li giustificano.
Caso 2: la software house che ha perso 18 mesi sull'architettura sbagliata
Una software house ha convinto un cliente del settore manifatturiero a riscrivere il loro gestionale legacy come sistema a microservizi, presentandolo come la "scelta moderna". Il team era di 6 developer, nessuno con esperienza precedente con Kubernetes o sistemi distribuiti. Il dominio aveva requisiti di transazionalità forte tra i moduli (ordini, magazzino, fatturazione sono fortemente accoppiati per definizione).
Dopo 18 mesi, il sistema era in produzione ma con problemi gravi: latenza elevata per le operazioni che attraversavano più servizi, problemi di consistenza dei dati nelle operazioni multi-step, e un costo operativo su Azure che era 4 volte il budget previsto. La risoluzione è stata una parziale consolidazione dei servizi in un Modular Monolith, con solo i moduli di reporting e integrazione terze parti rimasti come servizi separati.
La lezione: l'architettura deve seguire il problema reale, non la moda tecnologica. In questo caso, un Modular Monolith avrebbe fornito tutti i benefici necessari senza i costi che hanno quasi affondato il progetto.
Come prendere la decisione nel tuo team: un framework decisionale pratico
Un framework decisionale pratico si basa su tre domande chiave da rispondere onestamente basandosi sulla situazione attuale del progetto, non su scenari ipotetici futuri.
Domanda 1: quanti team devono rilasciare indipendentemente?
Questa è la domanda più importante. Se hai un singolo team o team che lavorano in modo coordinato, il monolite (o Modular Monolith) è la scelta quasi sempre corretta. Se hai 3 o più team che devono poter rilasciare autonomamente senza aspettarsi a vicenda, i microservizi iniziano ad avere senso organizzativo.
Una variante di questa domanda: hai già problemi di coordinamento tra team che causano rallentamenti misurabili? Se no, il problema organizzativo non è ancora abbastanza grave da giustificare la complessità aggiuntiva.
Domanda 2: hai componenti con requisiti di scalabilità davvero diversi oggi?
"Potremmo avere bisogno di scalare X in futuro" non è un segnale valido. Il segnale valido è: "Il componente X ha oggi requisiti di risorse 10x diversi dagli altri componenti, e questo sta causando sprechi economici o problemi di performance". Se questo è vero per alcuni componenti specifici, estrarre solo quelli come servizi ha senso. Non tutto.
Domanda 3: il team ha le competenze per gestire sistemi distribuiti?
Questa domanda va posta onestamente. Gestire Kubernetes in produzione, implementare distributed tracing, progettare il fallimento parziale, gestire la consistenza eventuale: queste sono competenze specifiche che richiedono tempo per essere acquisite. Se il team non le ha, il costo di apprenderle nel contesto di un sistema in produzione è alto e rischioso.
Se rispondi "no" alle prime due domande, parti con il Modular Monolith. Se rispondi "no" alla terza, parti con il Modular Monolith indipendentemente da quanto rispondi alle prime due. Puoi sempre estrarre servizi in seguito quando hai i segnali reali, le competenze e l'infrastruttura necessari.
La decisione nel contesto italiano
Nel mercato italiano, la maggior parte delle software house lavora su progetti dove il team non supera i 10 developer, i domini sono relativamente transazionali (gestionali, portali, piattaforme B2B), e il budget operativo è limitato. In questo contesto, il Modular Monolith è quasi sempre la scelta corretta. I microservizi vanno considerati solo quando si è già operativi con un sistema che ha dimostrato concretamente i limiti del monolite.
Diventare un Software Architect significa saper fare esattamente questo: prendere decisioni architetturali basate su evidenze, non su tendenze. Se vuoi sviluppare questa competenza in modo sistematico, l'articolo su come diventare Software Architect fornisce un percorso concreto.
Anche la conoscenza dei pattern architetturali classici è fondamentale per questa competenza: l'articolo sui pattern architetturali software copre i building block che si ritrovano in tutti i sistemi distribuiti e monolitici ben progettati.
Il Modular Monolith in .NET con ASP.NET Core: struttura concreta e comunicazione tra moduli
Il Modular Monolith viene spesso presentato come concetto, raramente come implementazione concreta. Questa sezione colma il gap: come si struttura effettivamente un progetto ASP.NET Core con architettura modulare, come i moduli comunicano tra loro, e come si mantengono i confini nel tempo.
La struttura di base in una solution .NET usa progetti separati per ogni modulo. Ogni modulo ha tre progetti: il core con le entità di dominio e le interfacce, l'infrastructure con le implementazioni concrete (EF Core, repository, client HTTP), e i contratti pubblici che espongono solo le interfacce accessibili agli altri moduli.
Struttura della solution e dei progetti
Una solution per un sistema e-commerce con tre moduli potrebbe avere questa struttura:
ECommerce.sln
|
+-- src/
| +-- Host/
| | +-- ECommerce.Api/ # ASP.NET Core host, Program.cs
| |
| +-- Modules/
| | +-- Catalog/
| | | +-- ECommerce.Catalog.Contracts/ # Interfacce pubbliche, DTO condivisi
| | | +-- ECommerce.Catalog.Core/ # Dominio, servizi, interfacce interne
| | | +-- ECommerce.Catalog.Infrastructure/ # EF Core, repository, integrazioni
| | |
| | +-- Orders/
| | | +-- ECommerce.Orders.Contracts/
| | | +-- ECommerce.Orders.Core/
| | | +-- ECommerce.Orders.Infrastructure/
| | |
| | +-- Payments/
| | +-- ECommerce.Payments.Contracts/
| | +-- ECommerce.Payments.Core/
| | +-- ECommerce.Payments.Infrastructure/
| |
| +-- Shared/
| +-- ECommerce.Shared.Kernel/ # Value objects, eventi di dominio base, etc.
|
+-- tests/
+-- ECommerce.Catalog.Tests/
+-- ECommerce.Orders.Tests/
+-- ECommerce.Integration.Tests/Il progetto Contracts di ogni modulo è l'unico progetto che altri moduli possono referenziare. Il modulo Orders può dipendere da ECommerce.Catalog.Contracts, ma non da ECommerce.Catalog.Core o ECommerce.Catalog.Infrastructure. Questo confine è la differenza tra un Modular Monolith e un monolite tradizionale dove tutto dipende da tutto.
Registrazione dei moduli nel DI container
Ogni modulo espone un metodo di estensione che registra tutte le proprie dipendenze interne. Il progetto host chiama questi metodi in Program.cs senza sapere nulla dei dettagli interni di ogni modulo.
// ECommerce.Catalog.Infrastructure/CatalogModule.cs
public static class CatalogModule
{
public static IServiceCollection AddCatalogModule(
this IServiceCollection services,
IConfiguration configuration)
{
// Registrazione DB context del modulo Catalog
// Ogni modulo ha il proprio schema nel database condiviso
services.AddDbContext<CatalogDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("Default"),
sql => sql.MigrationsAssembly(
typeof(CatalogDbContext).Assembly.FullName)));
// Registra i servizi interni (non accessibili dall'esterno del modulo)
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IInventoryRepository, InventoryRepository>();
// Registra l'interfaccia pubblica (definita in Contracts)
services.AddScoped<ICatalogService, CatalogService>();
services.AddScoped<IProductSearchService, ProductSearchService>();
return services;
}
}
// ECommerce.Catalog.Infrastructure/Data/CatalogDbContext.cs
public class CatalogDbContext : DbContext
{
// Ogni modulo lavora nel proprio schema del database
// "catalog" schema invece di "dbo"
private const string Schema = "catalog";
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(Schema);
modelBuilder.ApplyConfigurationsFromAssembly(
typeof(CatalogDbContext).Assembly);
}
}
// ECommerce.Api/Program.cs - host che non conosce i dettagli dei moduli
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddCatalogModule(builder.Configuration)
.AddOrdersModule(builder.Configuration)
.AddPaymentsModule(builder.Configuration);
builder.Services.AddControllers();Comunicazione tra moduli: contratti sincroni e eventi interni
La comunicazione tra moduli può avvenire in due modi. Per operazioni sincrone dove il modulo chiamante ha bisogno del risultato immediatamente, si usa la dipendenza sull'interfaccia pubblica definita nel progetto Contracts. Per operazioni asincrone o per ridurre l'accoppiamento, si usano eventi di dominio interni gestiti con un event bus in-process.
// ECommerce.Orders.Core/Services/OrderService.cs
// Il modulo Orders dipende solo da ICatalogService (dal progetto Contracts del Catalog)
public class OrderService
{
private readonly ICatalogService _catalogService; // Interfaccia pubblica di Catalog
private readonly IOrderRepository _orderRepository;
private readonly IEventBus _eventBus;
public OrderService(
ICatalogService catalogService, // Dal modulo Catalog.Contracts
IOrderRepository orderRepository, // Interno al modulo Orders
IEventBus eventBus)
{
_catalogService = catalogService;
_orderRepository = orderRepository;
_eventBus = eventBus;
}
public async Task<OrderResult> CreateOrderAsync(
CreateOrderCommand command, CancellationToken ct)
{
// Usa il contratto pubblico del modulo Catalog
// Non accede mai direttamente al DbContext o ai repository di Catalog
var productInfo = await _catalogService.GetProductInfoAsync(
command.ProductId, ct);
if (!productInfo.IsAvailable || productInfo.Stock < command.Quantity)
return OrderResult.Failure("Prodotto non disponibile nella quantità richiesta");
var order = Order.Create(command, productInfo.Price);
await _orderRepository.SaveAsync(order, ct);
// Pubblica un evento interno: il modulo Payments lo ascolta
// per avviare il processo di pagamento
await _eventBus.PublishAsync(
new OrderCreatedEvent(order.Id, order.Total, command.PaymentMethod), ct);
return OrderResult.Success(order.Id);
}
}
// Event bus in-process con MediatR
// ECommerce.Shared.Kernel/Events/IEventBus.cs
public interface IEventBus
{
Task PublishAsync<TEvent>(TEvent @event, CancellationToken ct = default)
where TEvent : IDomainEvent;
}
// ECommerce.Payments.Core/Handlers/OrderCreatedEventHandler.cs
// Il modulo Payments ascolta l'evento senza dipendere direttamente da Orders
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
private readonly IPaymentService _paymentService;
public OrderCreatedEventHandler(IPaymentService paymentService)
{
_paymentService = paymentService;
}
public async Task Handle(
OrderCreatedEvent notification, CancellationToken ct)
{
await _paymentService.InitiatePaymentAsync(
notification.OrderId, notification.Total, notification.PaymentMethod, ct);
}
}Questo pattern permette di migrare in futuro: se il modulo Payments cresce abbastanza da giustificare l'estrazione come microservizio, l'event bus in-process diventa un message broker reale (RabbitMQ, Azure Service Bus), e l'interfaccia IEventBus viene reimplementata con una versione che invia messaggi alla rete invece che al processo locale. Il codice dei moduli non cambia.
Quando il Modular Monolith diventa insufficiente
Il Modular Monolith diventa insufficiente quando i segnali organizzativi diventano concreti: più team che si bloccano a vicenda, componenti con requisiti di scalabilità radicalmente diversi confermati dalla produzione, o requisiti di compliance che richiedono isolamento fisico. A quel punto, i confini definiti dai moduli diventano le linee di estrazione per i microservizi. Il refactoring è chirurgico, non una riscrittura: il modulo Payments diventa il servizio Payments, con il proprio deployment, il proprio database e la propria pipeline CI/CD.
Migrazione realistica dal monolite ai microservizi: timeline, database split e costi nascosti
Il pattern Strangler Fig descrive la strategia a livello alto. Ma come si traduce in pratica, settimana per settimana, per un team italiano di 5-10 developer? La risposta onesta è che la migrazione richiede molto più tempo di quanto venga comunicato alle conferenze, e il database split è di gran lunga la parte più difficile.
Perché la migrazione big-bang non funziona
La migrazione big-bang è quella in cui si ferma lo sviluppo di nuove funzionalità, si riscrive il sistema da zero come microservizi, e si fa un cutover. In teoria sembra pulita. In pratica, il sistema legacy continua ad acquisire nuove funzionalità durante la riscrittura (perché il business non si ferma), i requisiti cambiano durante il processo, e al momento del cutover la versione riscritta è già indietro rispetto al sistema originale. Amazon stessa ha provato e ha documentato i fallimenti di questo approccio.
La migrazione incrementale con Strangler Fig è più lenta sul singolo servizio, ma permette di mantenere il sistema in produzione durante l'intero processo. Ogni servizio estratto porta un beneficio misurabile, e il rischio di ogni singolo step è limitato.
Il database split: la parte più difficile
Estrarre il codice di un modulo in un servizio separato è relativamente semplice. Estrarre il database è dove la maggior parte delle migrazioni si inceppa. In un monolite, i moduli condividono spesso tabelle, usano JOIN cross-modulo, e hanno constraint di foreign key tra entità di domini diversi.
Il processo di database split avviene in fasi:
Fase 1: eliminare i JOIN cross-modulo. Identifica tutte le query che eseguono JOIN tra tabelle di moduli diversi. Sostituiscile con due query separate e unisci i risultati in memoria nel codice applicativo. Questa fase è dolorosa ma necessaria: non puoi separare i database se ci sono JOIN tra di essi.
Fase 2: rimuovere i foreign key cross-modulo. I foreign key cross-modulo impediscono la separazione fisica dei database. Sostituiscili con foreign key "soft" (solo il valore dell'ID, senza il constraint di database). La consistenza referenziale diventa una responsabilità del codice applicativo e degli event handlers.
Fase 3: duplicare i dati di riferimento. Alcuni dati vengono letti da molti moduli: ad esempio, le informazioni di base di un prodotto potrebbero servire al modulo Ordini, Inventario e Fatturazione. Invece di avere un unico record condiviso, ogni modulo mantiene una proiezione locale dei dati che gli servono, aggiornata tramite eventi di dominio.
Fase 4: migrare fisicamente il database. Solo dopo aver completato le fasi precedenti si può estrarre le tabelle del modulo in un database separato. Questo richiede di aggiornare le connection string del servizio estratto e di verificare che non esistano dipendenze residue verso il database principale.
Timeline realistica per un team di 5-10 developer
Le timeline che seguono assumono un sistema monolitico di medie dimensioni (200k-500k righe di codice), con un team di 5-10 developer di cui 1-2 senior con esperienza di sistemi distribuiti. Non assumono che la migrazione sia l'unica attività: il team continua a sviluppare nuove funzionalità durante il processo.
Mesi 1-2: preparazione. Modularizzazione interna del monolite (se non è già un Modular Monolith), eliminazione dei JOIN cross-modulo, rimozione dei foreign key cross-modulo, setup dell'infrastruttura di base (Kubernetes cluster, API Gateway, pipeline CI/CD per il primo servizio). Questo lavoro spesso richiede più tempo del previsto perché emergono dipendenze nascoste.
Mesi 3-6: estrazione del primo servizio. Scelta del bounded context più adatto all'estrazione (tipicamente il più indipendente, non il più complesso). Implementazione del pattern Strangler Fig con YARP come proxy. Rollout graduale con feature flags. Monitoraggio intensivo. Solo quando il servizio gestisce il 100% del traffico in produzione per almeno 4 settimane, si rimuove il codice dal monolite.
Mesi 6-18: estrazione dei servizi successivi. Ogni servizio successivo richiede 2-3 mesi per l'estrazione completa incluso il database split. La velocità aumenta con l'esperienza, ma non drasticamente: ogni bounded context ha le proprie dipendenze nascoste da risolvere.
I costi nascosti che nessuna conference talk include nel calcolo: il tempo del team per acquisire competenze Kubernetes e sistemi distribuiti (tipicamente 3-6 mesi di produttività ridotta), il costo incrementale dell'infrastruttura cloud (un cluster Kubernetes managed su Azure o AWS costa tra i 300 e 800 euro al mese solo per il control plane, prima dei nodi worker), e il costo delle pipeline CI/CD aggiuntive (ogni servizio ha la propria pipeline, i propri ambienti di staging, il proprio processo di release).
Per un team italiano di 5-10 developer che lavora su un progetto con budget operativo normale, la migrazione completa da monolite a microservizi richiede 18-36 mesi e un investimento totale in infrastruttura e produttività ridotta nell'ordine delle centinaia di migliaia di euro. Questo non significa che sia sbagliata: significa che deve essere giustificata da benefici altrettanto concreti e misurabili.
Se i segnali ci sono, la migrazione vale l'investimento. Se i segnali non ci sono, il Modular Monolith è la risposta corretta: ti permette di rimandare la complessità senza accumulare debito tecnico, e di estrarre servizi in futuro solo dove e quando diventa davvero necessario.
Per approfondire come le decisioni architetturali si inseriscono nel percorso di crescita professionale, consulta l'articolo su come diventare Software Architect: la capacità di scegliere l'architettura giusta per il contesto giusto è la competenza centrale di chi vuole progredire oltre il ruolo di sviluppatore senior.
