Quando un progetto inizia a crescere, prima o poi arriva la stessa domanda: “Non sarebbe meglio passare ai microservizi?”
Più utenti, più funzionalità, più integrazioni, magari un'app mobile da affiancare alla web application, processi asincroni, notifiche e API esterne.
A prima vista la risposta sembra quasi automatica: il progetto sta diventando grande, quindi bisogna dividerlo. Non necessariamente.
Un monolite può gestire applicazioni molto complesse. Il problema nasce quando monolite diventa sinonimo di codice completamente accoppiato.
Il problema non è avere un solo deploy
Immaginiamo una classica applicazione Laravel organizzata tra controller, model, service e repository. All'inizio funziona benissimo.
Poi arrivano clienti, appuntamenti, fatture, notifiche, documenti, pagamenti e integrazioni. La directory dei service inizia a contenere decine di classi, i controller utilizzano servizi appartenenti a parti completamente diverse dell'applicazione e modificare una funzionalità significa attraversare mezza codebase.
Il problema non è Laravel. E non è nemmeno il fatto che tutto venga distribuito insieme. Il problema sono i confini che non esistono più.
Organizzare il software intorno al dominio
Una possibile evoluzione consiste nello smettere di organizzare l'intera applicazione esclusivamente per tipologia tecnica e iniziare a ragionare per capacità: Customers, Appointments, Billing, Notifications, Documents.
Ogni modulo rappresenta un confine riconoscibile. Può contenere ciò che gli serve a livello Application, Domain, Infrastructure e Http senza obbligare tutto il progetto a vivere in cartelle globali enormi.
Non è una struttura da copiare necessariamente alla lettera. Il concetto importante è che il codice relativo allo stesso dominio dovrebbe avere un confine riconoscibile.
Tecnicamente continua a essere una sola applicazione Laravel, un repository, un deploy e potenzialmente anche un solo database. Concettualmente, però, abbiamo iniziato a separare le responsabilità.
Modularità non significa creare una Action per qualsiasi cosa
Qui entrano spesso in gioco Vertical Slice Architecture, Clean Architecture, Hexagonal Architecture e Domain-Driven Design. Sono approcci dai quali possiamo prendere ottime idee. Il problema nasce quando diventano regole da applicare meccanicamente.
Organizzare il software per feature non significa necessariamente creare CreateAppointmentAction, UpdateAppointmentAction, CancelAppointmentAction, ConfirmAppointmentAction, RescheduleAppointmentAction e una nuova classe per ogni verbo dell'applicazione.
Sulla carta può sembrare estremamente ordinato. Nella pratica possiamo finire per sostituire controller troppo grandi con centinaia di classi minuscole. Il numero di file aumenta, la navigazione aumenta e le astrazioni aumentano, ma la qualità dell'architettura non necessariamente migliora.
Preferisco che sia la complessità del comportamento a giustificare una nuova astrazione, non semplicemente l'esistenza di un endpoint.
Un controller può tranquillamente coordinare un'operazione semplice. Non c'è necessariamente bisogno di creare una Action, un DTO, un'interfaccia, un repository e un response object per salvare quattro campi nel database.
Quando invece l'operazione contiene regole di business importanti, viene richiamata da più punti oppure necessita di essere isolata e testata indipendentemente, estrarla in un servizio o in un caso d'uso dedicato inizia ad avere un motivo concreto.
Non creo un'astrazione perché l'architettura dice che dovrebbe esserci. La creo quando risolve un problema.
Il dominio dovrebbe contenere ciò che conta davvero
Prendiamo un appuntamento. Cambiare il nome visualizzato potrebbe essere un'operazione estremamente semplice. Calcolare invece se quell'appuntamento può essere spostato potrebbe coinvolgere disponibilità dell'operatore, sovrapposizioni, orari dell'attività, durata del servizio, regole di cancellazione e stato corrente.
Questa è logica che merita attenzione. Non perché stiamo seguendo Clean Architecture, ma perché rappresenta una regola reale del sistema.
È questo tipo di comportamento che voglio evitare di disperdere tra controller, query, listener e helper. Il dominio dovrebbe proteggere le parti dell'applicazione che hanno realmente valore.
Anche le dipendenze devono rispettare i confini
Supponiamo che Appointments debba notificare un cliente quando viene creato un appuntamento. La soluzione più immediata potrebbe essere chiamare direttamente il sistema delle notifiche. Funziona, ma aumentando queste dipendenze rischiamo lentamente di ricreare lo stesso monolite accoppiato.
In alcuni casi possiamo utilizzare un evento: il modulo degli appuntamenti comunica semplicemente che è stato creato un appuntamento. Un listener può inviare una mail, un altro aggiornare un CRM, un altro ancora inviare un messaggio WhatsApp.
Eventi e queue diventano particolarmente interessanti quando alcune operazioni possono essere eseguite in maniera asincrona. Ma anche qui vale la stessa regola: non tutto deve diventare un evento. A volte una semplice chiamata è esattamente ciò che serve.
Un monolite può scalare
Un altro equivoco comune è associare automaticamente molti utenti ai microservizi. Ma architettura applicativa e infrastruttura sono due problemi collegati, non la stessa cosa.
Prima di distribuire un sistema possiamo intervenire su indici e query del database, caching, Redis, queue e worker, CDN, object storage, replica del database, load balancing e scaling orizzontale.
La stessa applicazione Laravel può essere eseguita su più istanze dietro un load balancer. Continua a essere un monolite.
Monolite non significa necessariamente un'applicazione che gira su un solo server.
Non progettare oggi i microservizi che forse serviranno tra tre anni
È facile cadere nell'errore opposto: “potrebbe diventare grande”, “potremmo avere migliaia di utenti”, “un giorno potremmo dover separare questo modulo”. E improvvisamente un'applicazione che deve ancora dimostrare di avere un problema di scalabilità possiede Kubernetes, Kafka e diversi servizi indipendenti.
Abbiamo pagato subito il costo di un problema che ancora non abbiamo.
Distribuire un sistema introduce networking, autenticazione tra servizi, logging distribuito, tracing, retry, deployment indipendenti, versionamento dei contratti, consistenza dei dati e gestione dei fallimenti parziali.
Sono problemi accettabili quando stiamo ottenendo qualcosa in cambio. Molto meno quando li introduciamo soltanto perché “i microservizi scalano meglio”.
Preferisco partire dalla domanda opposta: qual è la soluzione più semplice che mi permette comunque di mantenere buoni confini?
Molto spesso la risposta può essere un monolite modulare: Laravel, PostgreSQL o MySQL, Redis quando serve, queue per i processi asincroni e una struttura del codice costruita intorno al dominio.
Poi misuriamo CPU, memoria, query lente, code, throughput, tempi di risposta e colli di bottiglia. Prima troviamo il problema. Poi scegliamo la tecnologia necessaria per risolverlo.
E se un giorno un modulo dovesse diventare un microservizio?
Immaginiamo un'applicazione composta da Customers, Appointments, Billing e Notifications. Con il tempo Notifications potrebbe iniziare a gestire una quantità enorme di email, WhatsApp, push notification e altri eventi, richiedendo un modello di scaling completamente diverso.
A quel punto abbiamo finalmente un motivo per valutare l'estrazione. L'applicazione principale continua a esistere e separiamo solamente ciò che ha realmente bisogno di esserlo.
Naturalmente l'operazione non è gratuita. Una chiamata PHP interna è estremamente diversa da una comunicazione attraverso la rete: bisogna gestire indisponibilità, retry, duplicazioni, timeout e consistenza.
Ma almeno non dobbiamo contemporaneamente capire dove finisce Notifications e dove inizia Appointments: quel confine esisteva già nel software.
Quando sceglierei davvero i microservizi?
Non utilizzerei come metrica il numero di righe di codice e nemmeno semplicemente il numero di utenti. Cercherei problemi concreti: un dominio con necessità di scaling differenti, team con cicli di deploy realmente indipendenti, esigenze tecnologiche diverse o confini di dominio sufficientemente chiari da poter vivere autonomamente.
Il vantaggio ottenuto deve superare il costo operativo della distribuzione. Prima, rischiamo semplicemente di trasformare problemi di organizzazione del codice in problemi di rete.
Clean Architecture, Vertical Slice e DDD sono strumenti
Un progetto non diventa automaticamente migliore perché contiene Domain, Application, Infrastructure e Presentation. Così come non diventa automaticamente migliore perché ogni operazione possiede una propria Action.
Le cartelle non sono l'architettura. Le interfacce non sono l'architettura. I pattern non sono l'architettura. Sono strumenti attraverso i quali possiamo esprimere determinate decisioni.
Da Vertical Slice possiamo prendere l'idea di mantenere vicine le cose che cambiano insieme. Da Clean Architecture la separazione tra business logic e dettagli infrastrutturali. Dal Domain-Driven Design l'attenzione verso il dominio e i suoi confini. Dall'architettura esagonale l'idea che database, framework e provider esterni siano dettagli sostituibili.
Ma non siamo obbligati ad applicare ogni pattern contemporaneamente. Altrimenti rischiamo di costruire un software perfettamente aderente a un diagramma e terribile da modificare.
La vera decisione architetturale
Davanti a un nuovo progetto Laravel abbastanza importante, non partirei dalla domanda: “Monolite o microservizi?”
Partirei da: “Quali sono i confini del mio dominio?”
Poi cercherei di mantenerli abbastanza chiari senza introdurre complessità che il progetto non richiede ancora.
Un controller semplice può rimanere semplice. Una regola di business complessa merita di essere isolata. Una queue viene introdotta quando abbiamo qualcosa che ha senso eseguire asincronamente. Un evento quando vogliamo realmente disaccoppiare comportamenti. Un microservizio quando abbiamo una ragione concreta per distribuirlo indipendentemente.
Il monolite può evolvere. Alcuni moduli possono rimanere insieme per tutta la vita del prodotto. Altri possono essere estratti quando esisterà finalmente una ragione concreta.
L'obiettivo non è costruire oggi l'architettura che immaginiamo possa servirci tra cinque anni. E nemmeno utilizzare più pattern possibili.
L'obiettivo è costruire software abbastanza semplice da capire oggi e abbastanza ben organizzato da poter cambiare domani.
Top comments (0)