Luca Mele
Luca Mele

Architecture

Micro-Frontend: quando hanno senso (e quando no)

Micro-Frontend: quando hanno senso (e quando no)
Torna a tutti gli articoli
·8 min di lettura
Ascolta il podcast generato dall'IA
Tutti i podcast →

I micro-frontend sono diventati uno dei pattern architetturali più dibattuti nello sviluppo frontend. I sostenitori dicono che abilitano deployment indipendenti dei team e scalabilità. I critici dicono che aggiungono complessità inutile. Entrambi hanno ragione — la domanda è quale compromesso conta di più per la vostra situazione specifica.

Ho una prospettiva unica su questo perché sono stato l'architetto principale di un'architettura micro-frontend per la strategia B2C di AXA Svizzera, e ho anche scelto di NON usare i micro-frontend in ruoli successivi. Entrambe le decisioni erano corrette nel loro contesto.

Il problema che i micro-frontend risolvono davvero

I micro-frontend risolvono bene un problema: l'indipendenza organizzativa. Quando avete più team che devono rilasciare funzionalità per la stessa applicazione rivolta agli utenti con tempistiche diverse, e il coordinamento tra questi team è diventato un collo di bottiglia, i micro-frontend possono aiutare.

Tutto qui. Non rendono la vostra app più veloce (di solito il contrario). Non rendono il vostro codice più pulito. Non riducono la complessità. Scambiano complessità tecnica con flessibilità organizzativa.

In AXA avevamo esattamente questo problema. Più team erano responsabili di diverse parti dell'esperienza cliente B2C: preventivi assicurativi, sinistri, gestione polizze e onboarding. Ogni team aveva la propria cadenza di sprint, le proprie priorità e il proprio calendario di rilascio. Un frontend monolitico significava che un bug nel modulo sinistri poteva bloccare un rilascio del modulo preventivi. I team passavano più tempo a coordinare i deployment che a sviluppare funzionalità.

Come l'abbiamo fatto in AXA

Il nostro approccio era pragmatico, non ideologico. Non abbiamo inseguito il sogno dei microservizi di «ogni team sceglie il proprio framework». Abbiamo standardizzato su React e TypeScript — la libertà era nell'indipendenza di deployment, non nella scelta tecnologica.

Ogni micro-frontend era il proprio bundle JS deployato sul server. Il codice vendor era estratto in un chunk condiviso per evitare la duplicazione delle dipendenze. Abbiamo costruito tooling Rollup personalizzato che permetteva di rilasciare tutto in una volta tramite AEM CMS, ma anche di permettere ai singoli team di sovrascrivere solo il loro bundle JS sul server per hotfix — senza release completa.

Un aggregatore collegava le app, e una classe di docking gestiva la comunicazione tra app — stato condiviso, contesto utente e token. Ogni app creava automaticamente la propria shell e poteva decidere se richiedere autenticazione o funzionare in modo anonimo. Il team preventivi poteva così deployare indipendentemente mentre il team sinistri lavorava al proprio ritmo.

Inoltre, ho fondato una libreria di componenti comune costruita con Web Components — agnostica al framework, così ogni team consumava gli stessi building block UI. Si integrava direttamente nell'architettura micro-frontend: UX coerente in tutte le app, mantenuta da un team dedicato allo style guide.

// Each micro-frontend = its own JS bundle on the server
// Vendor JS is shared across all apps in a common chunk
// Rollup builds each app independently

// apps/quotes/rollup.config.js → /server/static/quotes.js
// apps/claims/rollup.config.js → /server/static/claims.js
// shared/vendor.js             → /server/static/vendor.js

// Aggregator collects all apps and provides a docking class
// for cross-app communication (shared state, auth tokens)
class AppDocking {
  private shared = new Map<string, unknown>();
  set(key: string, value: unknown) { this.shared.set(key, value); }
  get<T>(key: string): T { return this.shared.get(key) as T; }
}

// Each app auto-creates its own shell and decides
// whether it needs an access token or runs anonymously
interface AppConfig {
  name: string;
  entry: string;             // /server/static/quotes.js
  container: string;         // #quotes-root
  requiresAuth: boolean;     // app decides this
}

// Deploy all apps at once via AEM CMS release,
// OR override individual JS on the server for hotfixes
// App X deploys → only /server/static/x.js gets replaced

I veri costi di cui nessuno parla

Ecco cosa le presentazioni alle conferenze tralasciano:

Lo stato condiviso è difficile. Davvero difficile. Quando due micro-frontend devono condividere il contesto utente, lo stato del carrello o i contatori delle notifiche, state essenzialmente costruendo un sistema distribuito nel browser. Abbiamo passato settimane a sincronizzare lo stato di autenticazione in modo affidabile tra i moduli senza race condition.

La coerenza UX è costosa. Quando i moduli vengono deployati indipendentemente, le incoerenze visive si insinuano. Il modulo A viene rilasciato con stili dei pulsanti aggiornati mentre il modulo B ha ancora quelli vecchi. Abbiamo risolto questo con la nostra libreria di style guide basata su Web Components — componenti agnostici al framework che ogni modulo consumava. Ma costruire e mantenere quella libreria è stato un investimento significativo.

L'overhead di performance è reale. Ogni modulo porta il proprio overhead di runtime. Anche con le dipendenze condivise estratte in un chunk comune, il caricamento iniziale è più pesante di un'applicazione singola. In AXA, il nostro Time to Interactive è aumentato di circa 800ms rispetto al monolite. Abbiamo ottimizzato con lazy loading e prefetching aggressivi, ma l'overhead non è mai scomparso completamente.

L'esperienza degli sviluppatori ne risente. Far girare l'applicazione completa in locale significa orchestrare più server di sviluppo. Il debugging attraverso i confini dei moduli è doloroso. I test di integrazione richiedono che tutti i moduli siano disponibili. Abbiamo costruito un tooling significativo per renderlo praticabile, che era esso stesso un onere di manutenzione.

Quando ho scelto di non usarli

In Migros, costruendo Bikeworld e Micasa, avevamo un problema simile in superficie: più negozi che condividono infrastruttura. L'istinto sarebbe stato di usare i micro-frontend. Ma ho scelto un monorepo PNPM con Turbo invece.

Perché? Perché il problema organizzativo era diverso. Avevamo un team che costruiva entrambi i negozi, non più team che necessitavano deployment indipendenti. I negozi condividevano un design system e parte dell'infrastruttura ma avevano pagine prodotto e flussi di checkout diversi. Un monorepo con package condivisi ci dava il riuso del codice senza la complessità di deployment.

In Vontobel, costruendo una singola piattaforma finanziaria complessa, i micro-frontend sarebbero stati puro overhead. Un team, un target di deployment, un ciclo di rilascio. Un'applicazione Next.js ben strutturata con confini di modulo chiari — imposti da regole di linting e code review, non da isolamento a runtime — era la risposta giusta.

In una grande banca svizzera, ho portato i micro-frontend in una direzione diversa: Webpack Module Federation con un monorepo basato su Turborepo per 3 team. A differenza dell'approccio Rollup personalizzato di AXA dove i bundle JS risiedevano sul server e potevano essere sovrascritti individualmente, Module Federation aggrega a runtime — la shell carica i moduli remoti on demand. Il monorepo ci dà tooling condiviso e standard di codice consistenti, mentre la federation dà a ogni team piena indipendenza di deployment. Due implementazioni molto diverse dello stesso principio: team che possono rilasciare senza aspettarsi a vicenda.

Il framework decisionale

Dopo aver vissuto con i micro-frontend e scelto alternative, ecco il mio framework per capire quando hanno senso:

Usate i micro-frontend quando: avete 4+ team che rilasciano per la stessa applicazione, il coordinamento dei deployment è diventato un collo di bottiglia misurabile (non solo un fastidio), i team hanno cadenze di rilascio genuinamente diverse, e avete la capacità ingegneristica per costruire e mantenere l'infrastruttura necessaria (shell app, librerie condivise, tooling, pipeline CI/CD).

Non usate i micro-frontend quando: avete meno di 3 team, volete usare framework diversi per modulo (il costo UX è troppo alto), la vostra applicazione ha forti dipendenze di dati tra moduli, non avete capacità dedicata di platform engineering, o li scegliete perché sono di tendenza piuttosto che perché avete sentito il dolore che risolvono.

// Alternatives that solve similar problems with less cost

// 1. Monorepo with package boundaries
// Good for: shared code, independent builds, one team
// pnpm-workspace.yaml + turborepo

// 2. Module-level code splitting in a single app
// Good for: lazy loading, team ownership areas
const AdminModule = lazy(() => import('./modules/admin'));
const QuotesModule = lazy(() => import('./modules/quotes'));

// 3. Feature flags for independent feature releases
// Good for: decoupling deployment from release
if (flags.newCheckout) {
  return <NewCheckout />;
}

L'architettura è questione di compromessi

La migliore architettura è la più semplice che risolve i vostri problemi reali. Non i problemi che potreste avere tra due anni. Non i problemi che aveva l'azienda che ha scritto quell'articolo su Medium. I vostri problemi, oggi.

I micro-frontend hanno risolto un problema reale in AXA — bundle Rollup personalizzati su un server, classe di docking per lo stato condiviso, deploy individuale tramite AEM CMS. In una banca, lo stesso problema ha ricevuto una soluzione diversa: Webpack Module Federation con aggregazione a runtime da un monorepo. Entrambi funzionano. L'implementazione differisce perché i vincoli differiscono. Un monorepo ha risolto un problema reale in Migros. Un'applicazione singola ben strutturata ha risolto un problema reale in Vontobel. Il pattern non è cambiato — capite prima il problema, poi scegliete la soluzione più semplice che lo affronta.

Se sembra YAGNI applicato all'architettura, è esattamente così. I principi sono gli stessi a ogni livello di astrazione.