Vai al contenuto

Pattern di decomposizione e migrazione

Questi pattern rispondono a due domande: come dividere un sistema in servizi con confini sensati, e come passare da un monolite esistente ai microservizi senza fermare il business. Ci sono anche i pattern di infrastruttura che spostano fuori dal codice i compiti ripetitivi (proxy, TLS, log).

Pattern Problema che risolve Usalo quando
Decompose by Business Capability Confini dei servizi arbitrari o tecnici L'organizzazione ha funzioni di business chiare e stabili
Decompose by Subdomain Un modello unico che non regge per tutti Il dominio è complesso e lo stesso termine cambia significato
Strangler Fig Riscrittura big bang troppo rischiosa Devi migrare un monolite in produzione un pezzo alla volta
Anti-Corruption Layer Il modello legacy contamina il nuovo servizio Il nuovo servizio deve parlare con un sistema vecchio o esterno
Branch by Abstraction Sostituire un componente senza branch lunghi Il cambiamento è interno al codice e serve rilascio continuo
Parallel Run Paura che il nuovo sistema sbagli Il risultato è critico e deve coincidere con il vecchio
Sidecar Codice ripetuto in ogni servizio per log, TLS, proxy Servizi in linguaggi diversi con esigenze comuni
Ambassador Logica di connessione verso l'esterno duplicata Molte chiamate in uscita con retry, auth, routing
Service Mesh Gestire sidecar a mano su decine di servizi Tanti servizi e serve mTLS, tracing, routing centralizzati
Self-contained Service vs Shared Library Una libreria condivisa accoppia i servizi di nascosto Devi decidere se condividere codice o duplicarlo
flowchart TD
    subgraph taglio["✂️ Come dividere"]
        cap["🏢 Decompose by Business Capability"]
        sub["🗺️ Decompose by Subdomain"]
    end
    subgraph migra["🚚 Come migrare senza fermarsi"]
        fig["🌿 Strangler Fig"]
        acl["🛡️ Anti-Corruption Layer"]
        bba["🔀 Branch by Abstraction"]
        par["⚖️ Parallel Run"]
    end
    subgraph infra["🧰 Cosa spostare fuori dal codice"]
        side["🛵 Sidecar"]
        amb["🤝 Ambassador"]
        mesh["🕸️ Service Mesh"]
    end
    subgraph cond["📚 Cosa condividere"]
        lib["📦 Self-contained Service vs Shared Library"]
    end

Da monolite a microservizi

Un monolite non è un errore. È la scelta giusta quando il team è piccolo, il dominio è ancora poco chiaro o il traffico è gestibile. I microservizi hanno senso quando più team devono rilasciare in modo indipendente, quando parti del sistema hanno bisogni di scalabilità molto diversi, o quando un guasto in una funzione non deve fermare le altre.

Il costo dei microservizi è alto: rete, consistenza dei dati, osservabilità, rilascio. Se il problema è solo "il codice è disordinato", la risposta è il monolite modulare: un solo processo, ma diviso in moduli con confini espliciti, ognuno con il proprio schema dati e un'interfaccia pubblica. Il monolite modulare è anche il miglior punto di partenza per una futura migrazione: i confini dei moduli diventano i confini dei servizi.

flowchart LR
    mono["Monolite"] --> modular["Monolite modulare"]
    modular --> micro["Microservizi"]
    modular -. "spesso basta fermarsi qui" .-> stop["Fine"]

Decompose by Business Capability

In una frase: Dividi il sistema seguendo le funzioni di business dell'azienda, non i livelli tecnici.

Problema che risolve: Molti sistemi sono divisi per tecnologia: un servizio "database", uno "API", uno "frontend". Ogni funzionalità nuova tocca tutti e tre e i team si bloccano a vicenda. Serve un criterio di divisione che segua il valore prodotto, così che una modifica di business resti dentro un solo servizio.

Come funziona:

flowchart LR
    azienda(["🏢 Azienda e-commerce"])
    azienda -->|"vendere"| vend["👔 Team Vendite - Catalogo, Ordini"]
    azienda -->|"incassare"| fin["💰 Team Finanza - Pagamenti"]
    azienda -->|"spedire e avvisare"| log["🚚 Team Logistica - Magazzino, Notifiche"]
flowchart TD
    azienda["E-commerce"] --> cat["Gestione catalogo"]
    azienda --> ord["Gestione ordini"]
    azienda --> pag["Gestione pagamenti"]
    azienda --> mag["Gestione magazzino"]
    azienda --> not["Notifiche clienti"]
    cat --> svcCat["Servizio Catalogo"]
    ord --> svcOrd["Servizio Ordini"]
    pag --> svcPag["Servizio Pagamenti"]
    mag --> svcMag["Servizio Magazzino"]
    not --> svcNot["Servizio Notifiche"]
  1. Elenca cosa fa l'azienda per generare valore: vendere prodotti, incassare, spedire, avvisare i clienti.
  2. Ogni capacità di business è stabile nel tempo, anche se la tecnologia cambia.
  3. Ogni capacità diventa un servizio con i propri dati e la propria API.
  4. Un team è responsabile di una o più capacità, dall'inizio alla fine.

Quando usarlo: - L'organizzazione ha reparti o funzioni di business chiare (vendite, logistica, amministrazione). - Vuoi che la struttura dei servizi rispecchi la struttura dei team. - Il dominio è relativamente semplice e i termini hanno un significato unico.

Quando NON usarlo / rischi: - Le capacità di business sono troppo ampie: "Gestione ordini" può diventare un mini monolite. - L'organigramma è instabile: i servizi seguirebbero le riorganizzazioni. - Non chiarisce cosa fare quando lo stesso concetto (es. "prodotto") è usato da più capacità.

Esempio pratico: Un e-commerce nato come monolite ha tre team che lavorano tutti sullo stesso codice. Si individuano le capacità: catalogo, ordini, pagamenti, magazzino, notifiche. Il team logistica prende Magazzino e Notifiche, il team vendite prende Catalogo e Ordini, il team finanza prende Pagamenti. Ogni team rilascia il proprio servizio senza coordinarsi con gli altri.

Pattern correlati: Decompose by Subdomain, Database per Service, Strangler Fig

Decompose by Subdomain

In una frase: Dividi il sistema seguendo i sottodomini del Domain-Driven Design, ognuno con il proprio modello e il proprio linguaggio.

Problema che risolve: La parola "prodotto" significa cose diverse per il catalogo (descrizione, foto, prezzo), per il magazzino (codice, peso, scaffale) e per gli ordini (riga, quantità, sconto). Un modello unico che accontenta tutti diventa enorme e fragile. Serve dividere il dominio in zone dove ogni termine ha un significato preciso.

Come funziona:

flowchart LR
    esperto(["🧑‍💼 Esperto di dominio"]) -->|"dice: prodotto"| cat["🛍️ Catalogo - nome, foto, prezzo"]
    esperto -->|"dice: prodotto"| ord["📦 Ordini - riga, prezzo al momento"]
    esperto -->|"dice: prodotto"| mag["🏭 Magazzino - codice, peso, scaffale"]
    nota["💡 Stessa parola, tre modelli diversi"]
    cat -.- nota
    ord -.- nota
    mag -.- nota
flowchart LR
    subgraph core["Core domain"]
        ordini["Ordini - bounded context"]
        pagamenti["Pagamenti - bounded context"]
    end
    subgraph supporting["Supporting domain"]
        catalogo["Catalogo - bounded context"]
        magazzino["Magazzino - bounded context"]
    end
    subgraph generic["Generic domain"]
        notifiche["Notifiche - bounded context"]
    end
    catalogo -- "Prodotto pubblicato" --> ordini
    ordini -- "Ordine confermato" --> pagamenti
    ordini -- "Ordine confermato" --> magazzino
    pagamenti -- "Pagamento accettato" --> notifiche
    magazzino -- "Spedizione pronta" --> notifiche
  1. Il dominio e-commerce viene diviso in sottodomini: core (dove l'azienda compete: ordini, pagamenti), supporting (necessari ma non distintivi: catalogo, magazzino), generic (comprabili sul mercato: notifiche).
  2. Ogni sottodominio ha un bounded context: un confine dentro cui il modello e i termini sono coerenti.
  3. "Prodotto" esiste in Catalogo, Ordini e Magazzino, ma con attributi diversi: non si condivide la classe, si condividono eventi.
  4. Le frecce sono le relazioni fra context, di solito eventi o API, con una traduzione sul confine.
  5. Ogni bounded context diventa un servizio (o un modulo del monolite modulare).

Quando usarlo: - Il dominio è complesso e gli esperti usano lo stesso termine con significati diversi. - Vuoi investire la qualità maggiore nel core domain e comprare o semplificare il resto. - I team hanno già fatto sessioni di modellazione (event storming) con il business.

Quando NON usarlo / rischi: - Il dominio è semplice: il DDD aggiunge vocabolario e cerimonia senza beneficio. - Confini sbagliati costano cari: un bounded context tagliato male genera chiamate continue fra servizi. - Richiede accesso agli esperti di dominio; senza di loro i confini sono inventati dagli sviluppatori.

Esempio pratico: Nel monolite la tabella prodotti ha 80 colonne usate da tutti. Si fa un event storming con il business e si scopre che il catalogo cambia prezzi e descrizioni, il magazzino cambia giacenze e posizioni, gli ordini vogliono solo nome e prezzo al momento dell'acquisto. Nascono tre bounded context; Ordini salva una copia del nome e del prezzo nella riga d'ordine e ascolta l'evento "Prodotto pubblicato" da Catalogo.

Pattern correlati: Decompose by Business Capability, Anti-Corruption Layer, Event Sourcing, Publish/Subscribe

Strangler Fig

In una frase: Metti una facciata davanti al monolite e sposta una funzionalità alla volta nei nuovi servizi, fino a spegnere il vecchio.

Problema che risolve: Riscrivere un monolite da zero e sostituirlo in un giorno è il modo più sicuro per fallire: mesi senza rilasci, funzionalità dimenticate, un cutover che non si può provare. Serve un modo per migrare mentre il sistema resta in produzione e ogni passo è reversibile.

Come funziona:

Illustrazione: il fico strangolatore avvolge il monolite in tre fasi, dalla facciata allo spegnimento

flowchart TD
    subgraph fase1["Fase 1 - facciata"]
        c1["Client"] --> f1["Facciata / Router"]
        f1 --> m1["Monolite"]
    end
    subgraph fase2["Fase 2 - migrazione graduale"]
        c2["Client"] --> f2["Facciata / Router"]
        f2 -- "/pagamenti" --> p2["Servizio Pagamenti"]
        f2 -- "/notifiche" --> n2["Servizio Notifiche"]
        f2 -- "tutto il resto" --> m2["Monolite"]
    end
    subgraph fase3["Fase 3 - spegnimento"]
        c3["Client"] --> f3["Facciata / Router"]
        f3 --> p3["Servizio Pagamenti"]
        f3 --> n3["Servizio Notifiche"]
        f3 --> o3["Servizio Ordini"]
    end
  1. Fase 1: si mette un router (API gateway, reverse proxy) davanti al monolite. Non cambia nulla per i client, ma ora ogni richiesta passa da un punto controllabile.
  2. Fase 2: si sceglie una funzionalità con pochi legami (es. Notifiche), si riscrive come servizio, e il router manda a lei solo quelle rotte. Il resto va ancora al monolite. Si ripete per Pagamenti, poi Ordini.
  3. Il monolite si "svuota" un pezzo alla volta; se un servizio nuovo ha problemi, il router torna a puntare al monolite.
  4. Fase 3: quando il monolite non serve più nulla, si spegne.

Quando usarlo: - Il monolite è in produzione e non puoi fermarlo. - Le funzionalità hanno punti di ingresso identificabili (URL, code, eventi). - Vuoi imparare facendo: la prima migrazione insegna come fare le altre.

Quando NON usarlo / rischi: - Il monolite condivide il database con tutto: spostare il codice senza spostare i dati crea un Distributed Monolith. - La migrazione si ferma a metà e si vive per anni con due sistemi: serve un piano con una fine. - Le funzionalità sono così intrecciate che non esiste un pezzo "con pochi legami" da cui partire.

Esempio pratico: L'e-commerce ha un monolite Django. Si mette Nginx davanti. Il primo pezzo migrato è Notifiche: le email di conferma ordine. Il nuovo servizio ascolta l'evento "Ordine confermato" e il monolite smette di inviare email. Poi tocca a Pagamenti: il router manda /api/pagamenti/* al nuovo servizio, che usa un Anti-Corruption Layer per leggere gli ordini ancora nel monolite. Dopo un anno il monolite gestisce solo il carrello, e viene riscritto per ultimo.

Pattern correlati: Anti-Corruption Layer, Branch by Abstraction, Parallel Run, API Gateway

Anti-Corruption Layer

In una frase: Un livello di traduzione che protegge il modello del nuovo servizio dal modello del sistema legacy o esterno.

Problema che risolve: Quando il nuovo servizio Pagamenti deve leggere ordini dal monolite, la tentazione è usare direttamente le sue tabelle o i suoi DTO. Così il modello vecchio, con i suoi nomi strani e i suoi campi nullable, entra nel nuovo codice e lo sporca. Dopo sei mesi il servizio nuovo è legato al vecchio quanto prima.

Come funziona:

flowchart LR
    pay["💳 Servizio Pagamenti"] -->|"chiede OrdineDaPagare"| acl["🛡️ Traduttore ACL"]
    acl -->|"legge ORD_TOT_LRD = 1.234,50"| legacy[("🏚️ Monolite legacy")]
    acl -->|"restituisce Decimal 1234.50"| pay
flowchart LR
    nuovo["Servizio Pagamenti - modello nuovo"] --> acl["Anti-Corruption Layer"]
    acl -- "traduce" --> legacy["Monolite - modello legacy"]
    subgraph acl_dettaglio["Dentro l'ACL"]
        adapter["Adapter - chiama il legacy"]
        translator["Translator - converte i dati"]
        facade["Facade - API pulita"]
    end
  1. Il servizio nuovo parla solo con la Facade dell'ACL, che espone concetti nel linguaggio nuovo (Pagamento, ImportoDovuto).
  2. L'Adapter sa come chiamare il legacy: query SQL, SOAP, file CSV, quello che c'è.
  3. Il Translator converte i dati legacy (ORD_TOT_LRD, stringhe con virgola decimale) nel modello nuovo (Decimal, OrdineId).
  4. Se il legacy cambia o viene spento, si cambia solo l'ACL. Il servizio nuovo non se ne accorge.

Quando usarlo: - Durante una migrazione Strangler Fig, fra servizi nuovi e monolite. - Integrando un sistema esterno (gestionale, ERP, gateway di pagamento) con un modello che non controlli. - Due bounded context con modelli molto diversi devono collaborare.

Quando NON usarlo / rischi: - I due modelli sono quasi uguali: l'ACL diventa codice che copia campi uno a uno. - È un pezzo in più da mantenere e testare; se il legacy cambia spesso, l'ACL cambia spesso. - Nasconde la complessità: chi legge il servizio nuovo non vede quanto è fragile l'integrazione.

Esempio pratico: Il monolite salva l'importo ordine in una colonna ORD_TOT_LRD come stringa "1.234,50" e lo stato in codici numerici (3 = confermato). Il servizio Pagamenti definisce OrdineDaPagare(id: OrdineId, importo: Decimal, confermato: bool). L'ACL legge dal monolite via API interna, converte la stringa in Decimal("1234.50"), mappa 3 su True, e restituisce l'oggetto pulito. Quando Ordini diventa un servizio nuovo con JSON moderno, si riscrive solo l'Adapter.

Pattern correlati: Strangler Fig, Decompose by Subdomain, Adapter, Facade

Branch by Abstraction

In una frase: Sostituisci un componente dentro il codice mettendo prima un'interfaccia davanti, poi scambiando l'implementazione sotto, senza branch Git lunghi.

Problema che risolve: Devi sostituire la libreria di invio email, o il repository che legge dal vecchio database, in tutto il codice. Farlo in un branch separato per tre settimane significa conflitti continui e nessun rilascio. Serve un modo per fare il cambiamento a piccoli passi sul ramo principale, con il sistema sempre funzionante.

Come funziona:

flowchart LR
    mail["✉️ Servizio Notifiche"] -->|"invia()"| iface["🔌 Interfaccia Notificatore"]
    iface --> flag{{"🚩 Feature flag"}}
    flag -->|"off"| smtp["📮 SMTP vecchio"]
    flag -->|"on"| prov>"☁️ API provider nuovo"]
flowchart TD
    subgraph passo1["Passo 1 - crea l'astrazione"]
        c1["Codice chiamante"] --> i1["Interfaccia Notificatore"]
        i1 --> v1["Implementazione vecchia - SMTP"]
    end
    subgraph passo2["Passo 2 - nuova implementazione dietro flag"]
        c2["Codice chiamante"] --> i2["Interfaccia Notificatore"]
        i2 -- "flag off" --> v2["Vecchia - SMTP"]
        i2 -- "flag on" --> n2["Nuova - API provider"]
    end
    subgraph passo3["Passo 3 - rimuovi la vecchia"]
        c3["Codice chiamante"] --> i3["Interfaccia Notificatore"]
        i3 --> n3["Nuova - API provider"]
    end
  1. Introduci un'interfaccia (Notificatore) e fai passare tutti i chiamanti da lì. La vecchia implementazione resta l'unica. Rilascia.
  2. Scrivi la nuova implementazione dietro la stessa interfaccia. Un feature flag decide quale usare. Rilascia con flag spento.
  3. Accendi il flag per un gruppo di utenti, poi per tutti. Se qualcosa va male, spegni il flag.
  4. Elimina la vecchia implementazione e il flag. Spesso si elimina anche l'interfaccia, se non serve più.

Quando usarlo: - Il cambiamento tocca molti punti del codice e richiederebbe giorni o settimane. - Vuoi rilasciare ogni giorno anche durante la sostituzione (trunk-based development). - Il componente da sostituire ha un confine chiaro o se ne può creare uno.

Quando NON usarlo / rischi: - Il cambiamento è piccolo: fallo direttamente. - Dimenticare il passo 4: restano due implementazioni e un flag per sempre. - L'interfaccia viene disegnata sulla vecchia implementazione e vincola la nuova a imitarla.

Esempio pratico:

from typing import Protocol

class Notificatore(Protocol):
    def invia(self, destinatario: str, testo: str) -> None: ...

class NotificatoreSmtp:
    def invia(self, destinatario: str, testo: str) -> None:
        ...  # implementazione vecchia

class NotificatoreProvider:
    def invia(self, destinatario: str, testo: str) -> None:
        ...  # nuova API del provider

def crea_notificatore(flag_nuovo_provider: bool) -> Notificatore:
    if flag_nuovo_provider:
        return NotificatoreProvider()
    return NotificatoreSmtp()

Il servizio Notifiche usa solo Notificatore. Il flag si accende dalla configurazione, prima per il 5% dei clienti, poi per tutti.

Pattern correlati: Strangler Fig, Parallel Run, Feature Toggle, Strategy

Parallel Run

In una frase: Esegui il sistema vecchio e il nuovo sulla stessa richiesta, usa il risultato del vecchio e confronta quello del nuovo.

Problema che risolve: Il nuovo servizio Pagamenti calcola importi, sconti, tasse. I test passano, ma i casi reali sono migliaia e nessuno è sicuro che il nuovo calcolo coincida sempre con il vecchio. Un errore in produzione costa soldi veri. Serve una prova sul traffico reale senza rischiare.

Come funziona:

flowchart LR
    cliente(["🧑 Cliente"]) -->|"calcola totale"| router["🚦 Router"]
    router --> old["🏚️ Pagamenti vecchio"]
    router --> new["💳 Pagamenti nuovo"]
    old -->|"risposta usata"| cliente
    old -->|"123.50"| cmp["⚖️ Comparatore"]
    new -->|"123.50"| cmp
    cmp --> rep[/"📋 Report differenze"/]
sequenceDiagram
    participant C as Client
    participant R as Router
    participant V as Pagamenti vecchio
    participant N as Pagamenti nuovo
    participant K as Comparatore
    C->>R: calcola totale ordine
    R->>V: calcola totale
    R->>N: calcola totale
    V-->>R: 123.50
    N-->>R: 123.50
    R-->>C: 123.50 (risposta del vecchio)
    R->>K: vecchio 123.50 / nuovo 123.50
    K->>K: registra uguale o diverso
  1. Il router riceve la richiesta e la manda a entrambi i sistemi.
  2. Al client torna sempre la risposta del vecchio, che è quello di cui ci si fida.
  3. La risposta del nuovo viene confrontata con quella del vecchio e registrata.
  4. Dopo giorni o settimane si guardano le differenze: ogni differenza è un bug nel nuovo o nel vecchio.
  5. Quando le differenze sono zero (o spiegate), si inverte: il nuovo risponde e il vecchio si spegne.

Quando usarlo: - Il risultato è critico: soldi, tasse, dati legali. - La funzione è deterministica o quasi: stesso input, stesso output atteso. - Hai un volume di traffico reale sufficiente a coprire i casi strani.

Quando NON usarlo / rischi: - L'operazione ha effetti collaterali (addebita una carta, invia un'email): eseguirla due volte è un danno. Serve isolare la parte di calcolo. - Raddoppia il carico e il costo per tutta la durata del confronto. - Le differenze legittime (orari, arrotondamenti, dati in movimento) fanno rumore e nascondono i bug veri.

Esempio pratico: Il nuovo servizio Pagamenti deve calcolare il totale con IVA e sconti. Per tre settimane ogni richiesta calcola_totale va a entrambi. Il comparatore trova 40 differenze su 200.000: tutte ordini con coupon cumulativi, che il nuovo arrotondava per difetto. Si corregge, si aspetta un'altra settimana a zero differenze, poi il nuovo diventa quello che risponde. L'addebito vero sulla carta resta fuori dal parallel run: lo fa solo il vecchio.

Pattern correlati: Strangler Fig, Branch by Abstraction, Canary Release

Sidecar

In una frase: Un container affiancato al servizio, nello stesso pod, che si occupa di compiti trasversali come log, TLS e proxy.

Problema che risolve: Ogni servizio deve raccogliere log, cifrare il traffico, esporre metriche, ritentare le chiamate. Se Ordini è in Python, Pagamenti in Java e Catalogo in Go, la stessa logica va scritta tre volte e tenuta allineata. Serve un modo per dare queste funzioni a tutti senza toccare il codice di nessuno.

Come funziona:

Illustrazione: la moto Ordini con il sidecar che porta log, TLS, metriche e proxy sulla stessa strada

flowchart LR
    subgraph pod["Pod Servizio Ordini"]
        app["Container Ordini - Python"]
        side["Container Sidecar - proxy"]
        app <-- "localhost" --> side
    end
    side -- "TLS, retry, metriche" --> pag["Servizio Pagamenti"]
    side -- "log" --> logs["Raccolta log"]
    side -- "metriche" --> mon["Monitoraggio"]
  1. Il servizio Ordini e il sidecar vivono nello stesso pod: condividono rete e dischi, quindi parlano via localhost.
  2. Ordini manda richieste HTTP semplici al sidecar; il sidecar aggiunge TLS, retry, timeout e le inoltra a Pagamenti.
  3. Il sidecar legge i log dell'applicazione e li spedisce al sistema centrale; espone le metriche.
  4. Il sidecar è lo stesso per tutti i servizi, in qualsiasi linguaggio: si aggiorna una volta e vale per tutti.

Quando usarlo: - Servizi in linguaggi diversi che hanno bisogno delle stesse funzioni trasversali. - Vuoi aggiornare la logica di rete o di log senza rilasciare i servizi. - Usi un orchestratore (Kubernetes) che rende naturale il concetto di pod.

Quando NON usarlo / rischi: - Hai pochi servizi, tutti nello stesso linguaggio: una libreria è più semplice. - Ogni sidecar consuma CPU e memoria: con centinaia di pod il costo è visibile. - Aggiunge un salto di rete per ogni chiamata e un componente in più da debuggare quando qualcosa non risponde.

Esempio pratico: Nel cluster dell'e-commerce ogni pod ha un sidecar Envoy. Il servizio Ordini chiama http://localhost:15001/pagamenti/autorizza in chiaro; Envoy cifra con mTLS, ritenta una volta su errore di rete e registra la latenza. Il team piattaforma aggiorna la versione di TLS cambiando l'immagine del sidecar, senza che il team Ordini rilasci nulla.

Pattern correlati: Ambassador, Service Mesh, Circuit Breaker, Log Aggregation

Ambassador

In una frase: Un proxy affiancato al servizio che gestisce tutte le chiamate in uscita verso l'esterno: routing, autenticazione, retry.

Problema che risolve: Il servizio Pagamenti chiama un gateway di pagamento esterno, con certificati client, chiavi API a rotazione, un endpoint di produzione e uno di test. Tutta questa logica finisce nel codice di business e va replicata in ogni servizio che parla con l'esterno. Serve un "ambasciatore" che rappresenti il servizio verso fuori.

Come funziona:

flowchart LR
    subgraph pod["📦 Pod Pagamenti"]
        app["💳 Pagamenti"] -->|"http localhost"| amb["🤝 Ambassador"]
    end
    secret[/"🔑 Secret: chiave API, certificato"/] --> amb
    amb -->|"mTLS + X-Api-Key"| gw>"☁️ Gateway pagamenti esterno"]
    amb -.->|"in ambiente test"| sandbox>"🧪 Sandbox del gateway"]
flowchart LR
    subgraph pod["Pod Servizio Pagamenti"]
        app["Pagamenti"] -- "http localhost" --> amb["Ambassador"]
    end
    amb -- "mTLS, chiave API, retry" --> gw["Gateway pagamenti esterno"]
    amb -. "in ambiente test" .-> sandbox["Sandbox del gateway"]
  1. Pagamenti chiama sempre localhost: non sa nulla di certificati, chiavi, URL esterni.
  2. L'Ambassador aggiunge le credenziali, apre la connessione cifrata e gestisce retry e timeout.
  3. In ambiente di test l'Ambassador punta alla sandbox: il codice di Pagamenti è identico.
  4. È un caso particolare di Sidecar specializzato nel traffico in uscita.

Quando usarlo: - Il servizio parla con sistemi esterni con autenticazione complessa o che cambia spesso. - Vuoi cambiare destinazione (test, produzione, fornitore nuovo) senza toccare il codice. - Un servizio legacy non sa fare TLS moderno o retry, e non puoi modificarlo.

Quando NON usarlo / rischi: - Il servizio fa una sola chiamata esterna semplice: una libreria HTTP basta. - Stesso costo del sidecar: risorse e un salto di rete in più. - Se l'Ambassador nasconde gli errori (retry infiniti), il servizio non si accorge che l'esterno è giù.

Esempio pratico: Il servizio Pagamenti chiama http://localhost:8081/autorizza. L'Ambassador conosce il certificato client, aggiunge l'header X-Api-Key letto da un secret, e inoltra a https://api.gateway-pagamenti.example/v2/authorize. Quando l'azienda cambia fornitore, si riconfigura l'Ambassador e si adatta solo il formato della richiesta.

Pattern correlati: Sidecar, Service Mesh, Anti-Corruption Layer, Retry, Proxy

Service Mesh

In una frase: Un'infrastruttura (Istio, Linkerd) che mette un sidecar proxy accanto a ogni servizio e li governa da un piano di controllo centrale.

Problema che risolve: Con cinque servizi i sidecar si configurano a mano. Con cinquanta, ogni cambio di policy (mTLS obbligatorio, timeout, routing del 10% del traffico alla nuova versione) va ripetuto ovunque. Serve un posto unico dove dire "tutte le chiamate fra servizi sono cifrate" e che valga subito per tutti.

Come funziona:

flowchart TD
    cp["🎛️ Control plane Linkerd"]
    cp -->|"policy mTLS"| p1["🛵 Proxy"]
    cp -->|"policy mTLS"| p2["🛵 Proxy"]
    cp -->|"policy mTLS"| p3["🛵 Proxy"]
    p1 --- ord["📦 Ordini"]
    p2 --- pay["💳 Pagamenti"]
    p3 --- mag["🏭 Magazzino"]
    p1 -->|"cifrato"| p2
    p1 -->|"cifrato"| p3
flowchart TD
    cp["Control plane - Istio / Linkerd"]
    cp -- "configura" --> p1["Proxy"]
    cp -- "configura" --> p2["Proxy"]
    cp -- "configura" --> p3["Proxy"]
    subgraph podOrd["Pod Ordini"]
        ord["Ordini"] <--> p1
    end
    subgraph podPag["Pod Pagamenti"]
        pag["Pagamenti"] <--> p2
    end
    subgraph podMag["Pod Magazzino"]
        mag["Magazzino"] <--> p3
    end
    p1 -- "mTLS" --> p2
    p1 -- "mTLS" --> p3
  1. Il data plane è l'insieme dei proxy sidecar: ogni chiamata fra servizi passa per due proxy, uno in uscita e uno in ingresso.
  2. Il control plane distribuisce ai proxy certificati, regole di routing, timeout, retry e policy di accesso.
  3. Il codice dei servizi non cambia: Ordini chiama http://pagamenti/autorizza e la mesh fa il resto.
  4. La mesh sposta fuori dal codice: mTLS, retry e timeout, Circuit Breaker, tracing distribuito, canary routing, autorizzazione fra servizi.

Quando usarlo: - Decine di servizi su Kubernetes e serve mTLS, osservabilità e routing uniformi. - Più team e più linguaggi: le policy devono valere per tutti senza librerie. - Requisiti di sicurezza (zero trust) che impongono cifratura e identità su ogni chiamata.

Quando NON usarlo / rischi: - Pochi servizi: la mesh è più complessa dell'applicazione che protegge. - Curva di apprendimento alta e debug difficile: un errore di configurazione della mesh ferma tutto. - Latenza e consumo aggiuntivi su ogni chiamata, moltiplicati per due proxy.

Esempio pratico: L'e-commerce ha 30 servizi. Il team piattaforma installa Linkerd. Con una regola si attiva mTLS fra tutti i servizi. Per rilasciare la versione 2 di Pagamenti si scrive una policy che manda il 10% del traffico alla nuova versione e si osservano errori e latenza nella dashboard della mesh, senza toccare né Ordini né Pagamenti.

Pattern correlati: Sidecar, Ambassador, Circuit Breaker, Distributed Tracing, Canary Release

Self-contained Service vs Shared Library

In una frase: Un servizio deve avere tutto ciò che gli serve per essere rilasciato da solo; una libreria condivisa con logica di business è un accoppiamento nascosto.

Problema che risolve: Per non duplicare codice, i team creano ecommerce-common con modelli, validazioni e client condivisi. Dopo un anno ogni modifica alla libreria richiede di rilasciare tutti i servizi insieme, e un bug nella libreria li rompe tutti. Il monolite è tornato, solo distribuito su più repository.

Come funziona:

flowchart LR
    subgraph male["🚫 Accoppiamento nascosto"]
        lib["📦 ecommerce-common - Ordine, calcola_sconto"] -->|"rilascio insieme"| o1["📦 Ordini"]
        lib -->|"rilascio insieme"| m1["🏭 Magazzino"]
    end
    subgraph bene["✅ Servizi autonomi"]
        o2["📦 Ordini - proprio Ordine"] -->|"chiama API calcola_sconto"| cat["🛍️ Catalogo"]
        util["🧰 Libreria tecnica - log, http"] -.-> o2
    end
flowchart LR
    subgraph male["Accoppiamento nascosto"]
        lib["Libreria common - modelli e regole"]
        lib --> o1["Ordini"]
        lib --> p1["Pagamenti"]
        lib --> m1["Magazzino"]
    end
    subgraph bene["Servizi autonomi"]
        o2["Ordini - proprio modello"]
        p2["Pagamenti - proprio modello"]
        m2["Magazzino - proprio modello"]
        util["Libreria tecnica - log, tracing, client HTTP"]
        util -.-> o2
        util -.-> p2
        util -.-> m2
    end
  1. A sinistra la libreria contiene il modello Ordine e le regole di sconto: se cambia una regola, cambia la libreria, e tutti i servizi devono aggiornarla e rilasciare insieme.
  2. A destra ogni servizio ha il suo modello, anche se somiglia a quello degli altri: la duplicazione è accettata perché i modelli evolveranno in direzioni diverse.
  3. Si condivide solo codice tecnico e stabile: log, tracing, client HTTP, autenticazione. Mai regole di business.
  4. Regola pratica: se una modifica alla libreria obbliga a rilasciare più servizi insieme, la libreria contiene qualcosa che non dovrebbe.

Quando usarlo: - Scegli il servizio autonomo quando il codice in questione è logica di dominio o un modello dati. - Scegli la libreria condivisa per codice tecnico, stabile e senza conoscenza del dominio. - Quando due servizi condividono una regola di business, chiediti se uno dei due dovrebbe esporla come API.

Quando NON usarlo / rischi: - La duplicazione portata all'estremo: dieci copie di una stessa validazione fiscale che devono cambiare nello stesso giorno per legge. - Libreria tecnica con versioni incompatibili fra servizi: serve una politica di versionamento chiara. - Il "modello condiviso" nei contratti (eventi, DTO): va versionato come API pubblica, non importato come classe.

Esempio pratico: ecommerce-common conteneva Prodotto, Ordine e calcola_sconto. Il team Ordini doveva aggiungere un campo a Ordine ma Magazzino non compilava più. Si decide: Prodotto e Ordine vanno in ogni servizio con solo i campi che gli servono; calcola_sconto diventa un endpoint del servizio Catalogo; in ecommerce-common restano solo il client HTTP con tracing e il formato dei log.

Pattern correlati: Decompose by Subdomain, Database per Service, Anti-pattern da riconoscere

Anti-pattern da riconoscere

Distributed Monolith. Servizi separati che devono essere rilasciati insieme perché condividono database, librerie di business o chiamate sincrone a catena. Hai tutti i costi dei microservizi e nessun beneficio. Segnale: un rilascio di Ordini richiede di coordinare e rilasciare anche Pagamenti e Magazzino nello stesso giorno.

Nano-services. Servizi così piccoli che il costo di rete, rilascio e monitoraggio supera il valore della funzione che contengono. Una funzione per servizio non è un'architettura, è frammentazione. Segnale: per completare un ordine servono dieci chiamate a dieci servizi, e nessuno sa quale servizio possiede il concetto "ordine".

Shared Database. Più servizi leggono e scrivono le stesse tabelle. Ogni cambio di schema spaventa tutti, e nessuno può cambiare tecnologia di storage. Segnale: una migrazione sulla tabella ordini richiede l'approvazione di tre team, o un servizio legge direttamente dalla tabella di un altro.

Chatty services. Due servizi che si scambiano decine di chiamate sincrone per completare una sola operazione. Il confine fra i due è nel posto sbagliato: in realtà sono una cosa sola. Segnale: il tracing mostra che una richiesta "mostra carrello" produce 40 chiamate fra Ordini e Catalogo, e la latenza totale è la somma di tutte.

Come scegliere

flowchart TD
    q1{"Hai gia un monolite in produzione da migrare?"}
    q1 -- No --> q2{"Il dominio e complesso con termini ambigui?"}
    q2 -- Si --> subdomain["Decompose by Subdomain"]
    q2 -- No --> q3{"Le funzioni di business sono chiare e stabili?"}
    q3 -- Si --> capability["Decompose by Business Capability"]
    q3 -- No --> modular["Resta su un monolite modulare"]
    q1 -- Si --> q4{"Il cambiamento e dentro un solo codebase?"}
    q4 -- Si --> bba["Branch by Abstraction"]
    q4 -- No --> strangler["Strangler Fig"]
    strangler --> q5{"Il nuovo deve parlare con il legacy?"}
    q5 -- Si --> acl["Anti-Corruption Layer"]
    strangler --> q6{"Il risultato e critico e deve coincidere?"}
    q6 -- Si --> parallel["Parallel Run"]
    q7{"Serve logica trasversale fuori dal codice?"}
    q7 -- "Pochi servizi, in uscita verso esterni" --> ambassador["Ambassador"]
    q7 -- "Pochi servizi, log TLS metriche" --> sidecar["Sidecar"]
    q7 -- "Decine di servizi" --> mesh["Service Mesh"]

Regola veloce: se non hai un monolite, scegli come tagliare (sottodominio o capacità) e valuta se il monolite modulare basta. Se hai un monolite, Strangler Fig è il percorso, con Anti-Corruption Layer e Parallel Run come protezioni. Sidecar, Ambassador e Service Mesh rispondono a una domanda diversa: quanta infrastruttura trasversale spostare fuori dal codice, in base al numero di servizi.