
Cambiavi il flusso di registrazione e si rompeva il carrello. Toccavi la logica dei pagamenti e il catalogo smetteva di caricare. Il team non poteva lavorare in parallelo perché tutto viveva nello stesso codebase, nello stesso deploy, nello stesso database. Ogni rilascio era un evento collettivo, con test manuali, preghiere e un piano B che consisteva nel fare rollback.
Il problema non era il codice in sé. Era che tutto dipendeva da tutto.
Il contesto
Il monolite era il cuore di una piattaforma e-commerce con social network integrato. Migliaia di utenti, decine di funzionalità intrecciate, un codebase che nessuno del team attuale aveva scritto. L'outsourcing aveva lasciato un'eredità pesante: poca documentazione, nessun test automatico, convenzioni inconsistenti, tabelle dedicate a un singolo vendor che sono evolute in tabelle di sistema mantenendo lo stesso nome.
In concreto, il database era un campo minato. Tabelle come "users" contenevano colonne usate sia dalla registrazione, sia dal social network, sia dal sistema di pagamento. Modificare una colonna per migliorare il flusso di checkout significava rischiare di rompere la logica dei profili utente, perché entrambi i moduli leggevano e scrivevano sugli stessi campi, spesso con query che facevano join su cinque o sei tabelle senza una ragione chiara. Nessun confine tra i domini: il codice che gestiva gli ordini chiamava direttamente le funzioni del catalogo, che a loro volta dipendevano dalla logica delle notifiche. Un grafo di dipendenze che nessuno aveva mai disegnato, perché nessuno lo conosceva per intero.
La pipeline di deploy era unica. Un singolo repository, un singolo processo di build, un singolo artefatto da rilasciare. Se un test falliva sul modulo notifiche, il fix urgente al modulo pagamenti restava bloccato. I rilasci si accumulavano, e quando finalmente uscivano erano enormi, pieni di cambiamenti non correlati. Più cambiamenti significava più rischio, più rischio significava più test manuali, più test manuali significava più tempo. Un ciclo che si autoalimentava.
Da architetto e CTO, il mio compito era trasformare quel sistema in qualcosa che potesse evolvere senza rompersi ad ogni deploy. Con un team che nel tempo è cresciuto fino a undici persone.
La scelta
La tentazione del "riscriviamo tutto da zero" era forte. Impossibile fermarsi. Troppo rischio, troppo tempo, e non ce l'avrebbero mai lasciato fare. L'approccio è stato lo strangler fig: svuotare il monolite pezzo per pezzo, estraendo funzionalità in microservizi, senza mai spegnere quello che già funzionava ma superarlo.
La prima domanda non era "cosa estraiamo?" ma "cosa ci fa più male?"
I domini con più accoppiamento, più bug, più rilasci bloccati: quelli prima.
Cosa ho fatto
Non tutto poteva essere disaccoppiato allo stesso modo. Alcune operazioni dovevano restare sincrone: il microservizio serviva il monolite, riceveva la richiesta, rispondeva in tempo reale. Per queste parti la chiave era scalare orizzontalmente, così che il nuovo servizio reggesse il carico senza diventare un collo di bottiglia.
Dove il disaccoppiamento poteva essere completo, SQS. Messaggi in coda, elaborazione asincrona, nessuna dipendenza temporale tra chi produce e chi consuma l'evento. Il monolite pubblicava un evento ("ordine creato", "utente registrato") e il microservizio lo gestiva per conto suo, nei suoi tempi.
Abbiamo valutato Kafka. L'idea era attraente: event streaming, replay, log immutabile. Ma nel nostro contesto i fastidi superavano i benefici. C'era troppo da sistemare prima di aggiungere un'infrastruttura complessa. SQS era managed, semplice, e risolveva il problema reale. A volte la scelta giusta è quella meno ambiziosa.
Per identificare i domini abbiamo mappato le dipendenze partendo dai dati: quali tabelle venivano lette e scritte da quali moduli, dove si concentravano i join cross-dominio, quali aree del codice generavano più bug. Non serviva un'analisi perfetta, serviva una mappa abbastanza buona per prendere la prima decisione.
Il primo dominio estratto è stato il catalogo prodotti. Non perché fosse il più semplice, ma perché era quello che generava più dolore: ogni modifica al catalogo rischiava di rompere il carrello, e il team che lavorava sulle schede prodotto era costantemente in conflitto con chi lavorava sugli ordini. Separare il catalogo significava liberare due sottogruppi in un colpo solo.
La prima estrazione è stata la più lenta. Ci sono volute circa sei settimane per portare il catalogo fuori dal monolite, contando la definizione dei confini, la creazione del nuovo servizio, la migrazione dei dati e il periodo di doppia scrittura per verificare la coerenza. Le estrazioni successive sono scese a tre, quattro settimane. Non perché i domini fossero più semplici, ma perché avevamo costruito un metodo: sapevamo quali domande fare, quali trappole evitare, come gestire il periodo di transizione in cui il monolite e il nuovo servizio coesistono.
Il pattern era sempre lo stesso: identificare un dominio, definire i confini, estrarre il servizio, collegarlo con eventi o chiamate dirette a seconda del caso, verificare che il monolite continuasse a funzionare senza quella parte. Poi ripetere.
Risultati
Il monolite si è svuotato gradualmente. Ogni estrazione rendeva il sistema più stabile e il team più autonomo.
- Deploy indipendenti. Prima della separazione, i rilasci avvenivano una volta a settimana, il venerdì, con tutto il team in standby. Dopo, ogni squadra poteva rilasciare il proprio servizio anche più volte al giorno, senza aspettare gli altri e senza coordinamento. Il catalogo andava in produzione senza che il team ordini ne sapesse nulla, e viceversa. Se qualcosa andava storto, il rollback era circoscritto: si tornava alla versione precedente di quel singolo servizio, non dell'intero sistema. Prima, un rollback significava riportare indietro anche le modifiche di tutti gli altri, con il rischio di perdere fix già rilasciati.
- Meno rotture a catena. Un bug nel servizio notifiche non trascinava giù il checkout. Un picco di carico sul social network non rallentava il catalogo. L'isolamento funzionava, e quando un servizio aveva problemi il resto della piattaforma continuava a operare.
- Velocità di sviluppo. Con confini chiari tra i domini, le persone sapevano dove mettere le mani senza paura di rompere qualcosa dall'altra parte. I nuovi sviluppatori potevano diventare produttivi in settimane invece che in mesi, perché dovevano capire un servizio, non l'intero sistema.
L'azienda non esiste più, per ragioni che non c'entrano con la tecnologia. Ma il sistema, nel periodo in cui ha operato, ha retto.
Quando non serve
Non tutto va disaccoppiato. Un monolite che funziona, con un team piccolo e requisiti stabili, è una scelta perfettamente valida. Ho visto più danni da microservizi introdotti troppo presto che da monoliti lasciati in pace.
Il segnale che qualcosa deve cambiare è quando il costo del cambiamento nel sistema attuale supera il costo dell'estrazione. Quando ogni feature richiede il doppio del tempo perché metà lo passi a gestire effetti collaterali. Quando il deploy diventa un rischio invece che una routine. Quando il team cresce e le persone si pestano i piedi a vicenda perché lavorano tutte sullo stesso artefatto.
Se non hai quel dolore, non hai quel problema. E se non hai quel problema, la prima cosa da fare è verificare che il problema esista davvero.
Se hai un monolite che ti tiene sveglio la notte, o un sistema dove tutto dipende da tutto e nessuno osa toccare niente... parliamone. Ti dico se vale la pena disaccoppiare, e da dove partire.

In sintesi (TL;DR)
- Un monolite ereditato dall'outsourcing dove ogni modifica rompeva funzionalità non correlate: il problema era l'accoppiamento, non il codice.
- Approccio strangler fig: estrarre i domini che fanno più male, pezzo per pezzo, senza mai spegnere il sistema esistente.
- SQS per il disaccoppiamento asincrono, chiamate dirette dove serviva sincronia. Kafka valutato e scartato: troppa complessità per il contesto.
- Risultato: deploy indipendenti, meno rotture a catena, team autonomi sui propri domini.