Quando i microservizi sono la scelta sbagliata per il tuo monolite Laravel: il caso di una PMI lombarda
Ad aprile 2025 mi ha chiamato il CTO di una PMI nel bresciano - una software house con 8 sviluppatori che mantiene un SaaS B2B per la gestione della supply chain nel settore automotive, circa 2.000 aziende clienti e un fatturato software di 1,8 milioni di euro. Il prodotto girava su un monolite Laravel 10 di circa 180.000 righe di codice, deployato su un cluster di due server Hetzner AX52 con bilanciamento tramite HAProxy. Il monolite aveva sette anni di vita, funzionava, generava fatturato, e il team lo conosceva bene. Ma era lento: alcune schermate del backoffice impiegavano 4-5 secondi a caricarsi, e il CTO - che aveva appena finito di leggere "Building Microservices" di Sam Newman - era convinto che la soluzione fosse una sola: spezzare tutto in microservizi.
Quattro mesi prima della mia chiamata, la PMI aveva ingaggiato una consultancy milanese per guidare la migrazione. Budget allocato: 150.000 euro. Dopo quattro mesi di lavoro e circa 120.000 euro spesi, la situazione era questa: un Auth Service parzialmente funzionante (un'istanza Laravel separata con Passport che serviva token JWT), un Notification Service che gestiva il 60% delle notifiche (il resto partiva ancora dal monolite), un Billing Service abbandonato a metà perché la logica di fatturazione era troppo intrecciata con il core. Il monolite originale era ancora in produzione, ma ora chiamava l'Auth Service via HTTP per ogni autenticazione - aggiungendo 80-120 millisecondi di latenza a ogni singola richiesta. I quattro sviluppatori dedicati alla migrazione non toccavano feature da quattro mesi. I quattro rimasti sul monolite rattoppavano bug senza poter fare refactoring perché "tanto fra poco migriamo." Il team era diviso, il prodotto era fermo, e il CTO stava iniziando a chiedersi se avesse fatto la scelta giusta.
La mia risposta, dopo due giorni di assessment del codice e delle infrastrutture, è stata diretta: no, non aveva fatto la scelta giusta. E non perché i microservizi siano una cattiva architettura in assoluto - ma perché per una PMI con 8 sviluppatori, un singolo prodotto e nessun problema di scaling a livello di team, i microservizi sono una soluzione a un problema che non esiste, al costo di crearne almeno cinque nuovi.
Quando ha davvero senso migrare un monolite Laravel a microservizi?
Quasi mai, per una PMI italiana con meno di 15-20 sviluppatori. Questo non è cinismo - è il risultato di vent'anni di osservazione diretta su progetti reali. I microservizi risolvono un problema specifico: il coordinamento tra team grandi e indipendenti che devono rilasciare a velocità diverse su componenti con cicli di vita diversi. Se hai 50 sviluppatori divisi in 8 team che lavorano su funzionalità ortogonali e ogni team ha bisogno di deployare indipendentemente senza bloccare gli altri, i microservizi hanno senso. Se hai 8 sviluppatori che lavorano tutti sullo stesso prodotto, seduti nella stessa stanza (o sulla stessa call), i microservizi ti danno: latenza di rete al posto di chiamate in-process, debugging distribuito al posto di stack trace leggibili, 2-3 pipeline CI/CD da mantenere al posto di una, e la necessità di un platform engineer a tempo pieno che la tua PMI non può permettersi.
Il dato più eloquente che ho trovato nella letteratura recente viene da un confronto tecnico approfondito tra microservizi e monoliti nel 2026: le aziende che hanno adottato microservizi prematuramente riportano un aumento medio del 35% nel tempo di debugging rispetto a un monolite modulare, e un fabbisogno di 2-4 ingegneri di piattaforma aggiuntivi. Per una PMI italiana, quei 2-4 stipendi aggiuntivi (a 45-60.000 euro l'anno lordi ciascuno) mangiano tutto il margine che il prodotto genera.
Stai cercando un Consulente Informatico esperto per valutare se la tua architettura Laravel è quella giusta? Nel mio profilo professionale trovi l'esperienza concreta su architetture monolitiche modulari, refactoring incrementale e decisioni di scaling per PMI.
Cosa abbiamo trovato sotto il cofano della migrazione fallita
Il primo passo del mio assessment è stato mappare esattamente cosa era stato fatto nei quattro mesi precedenti e cosa stava causando i problemi reali di performance del monolite originale - quelli che avevano innescato l'intera decisione di migrare.
L'Auth Service "indipendente" era un'applicazione Laravel separata che faceva esattamente una cosa: validare token JWT. Per ogni richiesta al monolite, il middleware eseguiva una chiamata HTTP all'Auth Service su rete interna. Tempo medio della chiamata: 85 millisecondi. Su una pagina del backoffice che faceva 12 richieste AJAX al caricamento, il solo overhead di autenticazione era passato da ~2 millisecondi (verifica locale del token con chiave pubblica in memoria) a ~1.020 millisecondi - un secondo intero buttato via in latenza di rete per un'operazione che non aveva nessun motivo di attraversare il filo.
Il Notification Service era peggio. Il 60% delle notifiche passava dal servizio nuovo, il 40% dal monolite. Due code Redis separate, due set di template, due logiche di retry. Quando un utente non riceveva una notifica, il team doveva controllare in due posti. Il tempo di debug di un singolo ticket "non ho ricevuto l'email" era passato da 5 minuti a 35 minuti.
Ma la scoperta più importante è stata un'altra: i problemi di performance del monolite - quelli che avevano motivato la migrazione - non avevano niente a che fare con l'architettura monolitica. Erano tre query Eloquent con pattern N+1 non risolti, un innodb_buffer_pool_size al valore di default su un server con 64 GB di RAM, e un modulo di reportistica che calcolava aggregati in PHP anziché con query SQL aggregate. Problemi che si risolvono in tre giornate di profiling e tuning, non in quattro mesi di rearchitecting. Ho descritto in dettaglio il metodo per diagnosticare e risolvere questi pattern nel mio articolo sulla riduzione di un checkout da 4,2 secondi a 280 millisecondi su Hetzner senza upgrade hardware.
Il modular monolith: l'architettura che il tuo monolite dovrebbe avere
La soluzione che ho proposto al CTO non era "torna al monolite com'era". Era: tieni il monolite, ma strutturalo in modo che i confini tra i domini di business siano espliciti, le dipendenze siano tracciate, e se un giorno servirà davvero estrarre un servizio, lo potrai fare senza riscrivere tutto.
Il concetto si chiama modular monolith (monolite modulare) e nel mondo Laravel si implementa con una combinazione di namespace dedicati per bounded context, service provider per modulo, e contratti (interface) per la comunicazione tra moduli. Martin Joo ha scritto la guida di riferimento più completa su Domain-Driven Design con Laravel, che descrive esattamente questa architettura con esempi concreti.
Sul progetto del cliente bresciano, la ristrutturazione ha seguito questo schema. Il monolite aveva tutto nel classico app/Models, app/Http/Controllers, app/Services - la struttura piatta che Laravel genera di default e che funziona fino a ~50.000 righe di codice, dopodiché diventa ingestibile. La nuova struttura:
app/
├── Modules/
│ ├── SupplyChain/ # Bounded context: gestione supply chain
│ │ ├── Models/
│ │ ├── Services/
│ │ ├── Contracts/ # Interface pubbliche del modulo
│ │ ├── Events/
│ │ ├── Listeners/
│ │ └── SupplyChainServiceProvider.php
│ ├── Billing/ # Bounded context: fatturazione
│ │ ├── Models/
│ │ ├── Services/
│ │ ├── Contracts/
│ │ └── BillingServiceProvider.php
│ ├── Auth/ # Bounded context: autenticazione
│ │ ├── Models/
│ │ ├── Services/
│ │ ├── Contracts/
│ │ └── AuthServiceProvider.php
│ └── Notification/ # Bounded context: notifiche
│ ├── Channels/
│ ├── Templates/
│ ├── Contracts/
│ └── NotificationServiceProvider.php
├── Http/Controllers/ # Controller rimangono qui, sottili
└── Shared/ # Codice condiviso tra moduliLa regola fondamentale: ogni modulo espone le proprie funzionalità tramite un'interface nel namespace Contracts, e gli altri moduli dipendono dall'interface, mai dall'implementazione concreta. Il service provider del modulo registra il binding interface → implementazione nel container IoC di Laravel:
// app/Modules/Billing/BillingServiceProvider.php
namespace App\Modules\Billing;
use Illuminate\Support\ServiceProvider;
use App\Modules\Billing\Contracts\InvoiceGenerator;
use App\Modules\Billing\Services\EloquentInvoiceGenerator;
class BillingServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
InvoiceGenerator::class,
EloquentInvoiceGenerator::class
);
}
}// app/Modules/SupplyChain/Services/OrderProcessor.php
namespace App\Modules\SupplyChain\Services;
use App\Modules\Billing\Contracts\InvoiceGenerator;
class OrderProcessor
{
public function __construct(
private readonly InvoiceGenerator $invoicing
) {}
public function complete(Order $order): void
{
// ... logica di completamento ordine
$this->invoicing->generate($order);
}
}Il vantaggio di questa struttura rispetto ai microservizi è che la chiamata $this->invoicing->generate($order) è una chiamata in-process - zero latenza di rete, zero serializzazione, zero rischio di timeout. Ma il giorno in cui il modulo Billing dovesse effettivamente diventare un servizio separato, basta creare un'implementazione alternativa dell'interface InvoiceGenerator che fa una chiamata HTTP anziché una query Eloquent, cambiare il binding nel service provider, e il resto del codice non cambia di una riga. Questa è la vera preparazione ai microservizi: non fare microservizi adesso, ma strutturare il codice in modo che farli domani sia un'operazione chirurgica anziché una riscrittura.
Come abbiamo gestito la transizione in due settimane
La prima settimana è stata dedicata a due operazioni parallele: il fix dei problemi di performance reali (N+1, buffer pool, aggregati SQL) e il rientro dell'Auth Service nel monolite. Quest'ultimo è stato sorprendentemente semplice: il middleware che chiamava l'Auth Service via HTTP è stato sostituito con il middleware standard di Laravel che verifica il token in-process. Il tempo di autenticazione è tornato da 85 ms a 1,8 ms per richiesta. L'Auth Service è stato spento e il DNS interno aggiornato. Nessun utente ha notato nulla, se non che il backoffice era "più veloce."
La seconda settimana è stata dedicata alla ristrutturazione modulare. Otto sviluppatori, divisi in quattro coppie, hanno migrato il codice dai namespace piatti (App\Models\*, App\Services\*) ai bounded context modulari. La migrazione è stata meccanica al 90% - spostare file, aggiornare namespace, aggiungere i service provider in config/app.php - e ha richiesto attenzione reale solo nei punti in cui due moduli erano accoppiati: query Eloquent che facevano JOIN tra tabelle di domini diversi, eventi che attraversavano confini di modulo senza un contratto esplicito, e servizi che dipendevano da implementazioni concrete anziché da interface. Questi punti di accoppiamento sono stati documentati come debito tecnico da risolvere nei sprint successivi, non come bloccanti della ristrutturazione.
Il Notification Service, che era stato parzialmente estratto, è stato riassorbito nel modulo Notification del monolite. I template duplicati sono stati unificati. La coda Redis è tornata una sola. Il tempo di debug di un ticket "non ho ricevuto l'email" è tornato a 5 minuti.
Per chi sta valutando come ottimizzare le query Eloquent prima di prendere decisioni architetturali drastiche, il consiglio che do sempre ai CTO è lo stesso: profila prima, ristruttura poi. Il 90% dei problemi di performance di un monolite Laravel non si risolve con un'architettura diversa - si risolve con indici, eager loading e configurazione corretta del database.
Quando i microservizi hanno senso - una checklist onesta
Non voglio lasciare l'impressione che i microservizi siano sempre sbagliati. Non lo sono. Sono uno strumento potente per un problema specifico. Il problema è che quel problema specifico è raro nelle PMI italiane. Ecco la checklist che uso con i clienti per decidere:
- Team superiore a 15-20 sviluppatori che lavorano su funzionalità indipendenti con cicli di rilascio diversi. Se tutti i tuoi sviluppatori rilasciano insieme, i microservizi ti danno complessità senza benefici.
- Requisiti di scaling radicalmente diversi tra componenti. Se il modulo di notifiche push deve gestire 100.000 messaggi al minuto ma il rest dell'applicazione serve 50 richieste al secondo, estrarre quel modulo come servizio autonomo ha senso. Se tutto scala uniformemente, no.
- Linguaggi o runtime diversi sono la scelta migliore per componenti specifici. Se il core è Laravel ma hai bisogno di un servizio di elaborazione video in Python o di un motore di raccomandazione in Go, l'estrazione è giustificata dal vincolo tecnologico.
- Hai già un platform team (o il budget per crearne uno) che gestisce CI/CD, service mesh, observability e incident response per un'architettura distribuita. Senza questo team, sei un'azienda che ha costruito un aereo senza assumere un pilota.
Se nessuna di queste condizioni è vera - e per la maggior parte delle PMI italiane non lo è - il modular monolith descritto in dettaglio nella guida di 200OK Solutions per Laravel 12 è l'architettura giusta. Ti dà confini puliti, indipendenza dei moduli, testabilità, e la possibilità concreta di estrarre un servizio il giorno in cui le condizioni cambieranno - senza pagare il costo dei microservizi oggi per un beneficio che forse arriverà tra tre anni. E quando quel giorno arriverà, se arriverà, lo Strangler Fig Pattern - che per chi sta valutando il passaggio a runtime persistenti significa anche esplorare se e quando Laravel Octane ha senso per la propria PMI - permette di estrarre un servizio alla volta senza riscrivere tutto.
Il risultato sul cliente bresciano, a due settimane dal mio intervento: performance del backoffice passata da 4-5 secondi a 600 millisecondi (fix N+1 + buffer pool + aggregati SQL), team riunificato su un'unica codebase, una sola pipeline CI/CD, una sola coda Redis, un solo sistema di monitoraggio. Il CTO ha recuperato quattro sviluppatori che per quattro mesi avevano lavorato su infrastruttura anziché su prodotto. Il budget residuo dei 150.000 euro originali - circa 30.000 euro - è stato investito in un ciclo di profiling trimestrale e in formazione interna su architetture modulari. A sei mesi di distanza, il monolite modulare gestisce il 40% di traffico in più rispetto a quando era stato deciso di migrare ai microservizi, senza nessun intervento infrastrutturale aggiuntivo. La migrazione ai microservizi non è stata cancellata - è stata rimandata a quando e se le condizioni la giustificheranno. E se quel giorno arriverà, il codice sarà già pronto, perché i confini tra i moduli sono espliciti e i contratti sono definiti. Contattami se stai affrontando una decisione architetturale simile: prima di investire mesi e budget in una migrazione, due giornate di assessment possono dirti se il problema è davvero l'architettura o se è qualcos'altro che si risolve in modo più semplice, più economico e più rapido.