Parti da un monolite modulare ed estrai i servizi sotto pressione
Inizia con un singolo deployable ben modularizzato ed estrai un servizio solo quando un bisogno concreto e attuale (scaling indipendente, cadenza di deploy indipendente, proprietà di team) lo impone. La decomposizione prematura trasforma chiamate in-process economiche in modalità di fallimento di rete e impone l'intera tassa operativa prima di avere i volumi che la giustifichino.
- Fai: rilascia un monolite modulare con confini di modulo espliciti, poi ritaglia l'unico modulo che ha davvero bisogno di scalare separatamente.
- Non fare: aprire un nuovo repo e servizio per ogni entità dal primo giorno, prima di comprendere il dominio o di avere CI/CD e osservabilità.
Allinea i confini dei servizi alle capacità di business e ai bounded context
Ritaglia i servizi lungo i bounded context del Domain-Driven Design e le capacità di business, così che ogni servizio possieda una fetta coesa del dominio e cambi per un solo insieme di ragioni. I confini tracciati lungo gli strati tecnici creano chiamate chiacchierone tra servizi e modifiche che si propagano su molti servizi.
- Fai: metti tutto ciò che riguarda gli "ordini" (regole, dati e API) in un solo servizio.
- Non fare: creare microservizi "controller", "service" e "repository" separati per la stessa capacità.
Dai a ogni servizio il proprio database
Ogni servizio deve possedere i propri dati in modo esclusivo ed esporli solo tramite la propria API o eventi; nessun altro servizio legge direttamente le sue tabelle. Un database condiviso accoppia silenziosamente i servizi così che non possano più distribuire o cambiare schema in modo indipendente, ricreando un monolite con la latenza di rete.
- Fai: fai sì che il servizio Ordini pubblichi eventi che il servizio Reportistica consuma nel proprio store.
- Non fare: lasciare che la Reportistica esegua join SQL sulle tabelle del servizio Ordini.
Metti un timeout e un circuit breaker su ogni chiamata remota
Ogni chiamata di rete può bloccarsi o fallire, quindi limitala con un timeout e proteggi le dipendenze cronicamente in errore con un circuit breaker che fallisce rapidamente e lascia recuperare la dipendenza. Le chiamate senza limiti esauriscono i thread e lasciano che una singola dipendenza lenta degeneri in un'interruzione dell'intero sistema.
- Fai: avvolgi un client HTTP/gRPC in uscita con un timeout più un circuit breaker Resilience4j e un fallback.
- Non fare: chiamare un servizio a valle con il timeout infinito predefinito del client e senza gestione dei fallimenti.
Riprova solo le operazioni idempotenti, con backoff e jitter
Riprova i fallimenti transitori con un numero limitato di tentativi, backoff esponenziale e jitter, ma solo per operazioni sicure da ripetere. Riprovare un'operazione non idempotente può duplicare effetti collaterali come addebitare due volte una carta, e i retry sincroni senza jitter possono produrre un thundering herd che peggiora l'interruzione.
- Fai: rendi idempotente un endpoint di "creazione ordine" tramite una chiave di idempotenza, poi riprova in sicurezza.
- Non fare: riprovare ciecamente una POST che crea un record senza deduplicazione.
Pubblica gli eventi in modo affidabile con la transactional outbox
Per cambiare lo stato ed emettere un evento in modo atomico, scrivi l'evento in una tabella outbox dentro la stessa transazione locale, poi inoltralo al broker con un processo separato o con change data capture. Fare la scrittura su database e la pubblicazione sul broker come due passi indipendenti finisce per perdere o duplicare eventi quando un crash cade tra i due.
- Fai: inserisci nella tabella
outboxnella stessa transazione dell'aggiornamento di business, poi pubblica dall'outbox. - Non fare: salvare la riga e poi chiamare il broker nel codice applicativo sperando che entrambi vadano a buon fine.
Rendi idempotenti i consumatori di messaggi
Broker e retry consegnano i messaggi at-least-once, quindi i consumatori devono tollerare i duplicati rilevando e ignorando i messaggi già elaborati. Un consumatore che assume la consegna exactly-once applicherà due volte gli effetti ogni volta che un messaggio viene riconsegnato dopo un fallimento transitorio.
- Fai: registra gli ID dei messaggi elaborati (o usa una chiave naturale di deduplicazione) e salta i duplicati.
- Non fare: assumere che ogni evento sia consegnato esattamente una volta e applicarlo incondizionatamente.
Propaga un correlation ID e strumenta il tracing distribuito
Genera un correlation/trace ID al bordo del sistema e propagalo attraverso ogni chiamata e ogni riga di log, strumentando con OpenTelemetry. Senza tracing end-to-end, diagnosticare quale hop ha causato un fallimento o un picco di latenza tra molti servizi diventa un tirare a indovinare.
- Fai: standardizza sulla propagazione OpenTelemetry e includi gli ID di trace e span nei log strutturati.
- Non fare: scrivere log per servizio senza un identificatore di richiesta condiviso.
Evolvi le API in modo retrocompatibile
All'interno di una major version aggiungi solo campi opzionali e nuovi endpoint; riserva rimozioni, rinomine e validazioni più stringenti a una nuova major version fatta girare in parallelo durante la migrazione. Rompere un contratto pubblicato sul posto costringe ogni consumatore a ridistribuire in lock-step, distruggendo la distribuibilità indipendente che giustifica i microservizi.
- Fai: aggiungi un nuovo campo opzionale e un endpoint
v2mantenendov1finché i consumatori migrano. - Non fare: rinominare un campo JSON nella versione esistente e rompere tutti i client in un colpo.
Verifica l'integrazione con i test dei contratti consumer-driven
Usa i contratti consumer-driven così che ogni consumatore dichiari ciò di cui ha bisogno e il provider lo verifichi in CI, intercettando le modifiche breaking prima del deploy senza suite end-to-end lente e fragili. Affidarsi solo a test end-to-end completi tra molti servizi produce pipeline instabili e feedback lento.
- Fai: mantieni i contratti Pact e fai fallire la build del provider se le aspettative di un consumatore non sono soddisfatte.
- Non fare: dipendere da un ambiente di staging fragile che esegue tutti i servizi per intercettare le rotture di contratto.
Progetta per la degradazione controllata e il bulkheading
Decidi in anticipo come si comporta ogni servizio quando una dipendenza non è disponibile, restituendo dove possibile una risposta degradata ma utile, e isola le risorse così che una dipendenza in difficoltà non possa consumarle tutte. I sistemi che trattano ogni dipendenza come sempre disponibile falliscono in modo netto e totale invece di degradare.
- Fai: servi raccomandazioni in cache o di default quando il servizio di raccomandazione è giù, usando un pool di thread separato per esso.
- Non fare: lasciare che la pagina prodotto restituisca un 500 perché un servizio opzionale per la sidebar è andato in timeout sul pool condiviso.
Esternalizza la configurazione e mantieni i servizi stateless
Leggi configurazione e segreti dall'ambiente o da un servizio di config/secret, e mantieni le istanze di servizio stateless così che qualunque istanza possa gestire qualunque richiesta e possa essere scalata o sostituita liberamente. Inserire la config nelle immagini o tenere lo stato di sessione in memoria blocca lo scaling orizzontale e le istanze sicure e usa-e-getta.
- Fai: inietta la config tramite variabili d'ambiente e conserva lo stato di sessione in una cache condivisa o in un token.
- Non fare: hardcodare endpoint e credenziali nell'immagine o affidarti a sticky session per lo stato in memoria.
Dai a ogni servizio la propria pipeline di deployment
Ogni servizio dovrebbe costruire, testare e distribuire tramite la propria pipeline automatizzata così da poter rilasciare col proprio ritmo senza coordinare un deploy big-bang. Una singola pipeline condivisa che rilascia tutti i servizi insieme è il marchio di un monolite distribuito e rimuove il principale beneficio dell'architettura.
- Fai: distribuisci il servizio Pagamenti in modo indipendente dietro health check e una canary, anche più volte al giorno se serve.
- Non fare: richiedere che ogni servizio sia versionato e rilasciato insieme in un unico evento orchestrato.
Metti in sicurezza il traffico servizio-servizio con zero-trust e mTLS
Tratta la rete interna come ostile: autentica e cifra le chiamate servizio-servizio con mutual TLS, autorizza in base all'identità propagata invece che alla posizione di rete, e tieni i segreti in un manager. Assumere che tutto dentro il perimetro sia fidato lascia a un servizio compromesso muoversi liberamente nel sistema.
- Fai: imponi mTLS e autorizzazione basata sull'identità (spesso tramite un service mesh), propagando l'identità utente in token firmati.
- Non fare: permettere a qualunque servizio nel cluster di chiamare qualunque altro senza autenticazione e in chiaro.
Esponi health check di liveness e readiness
Fornisci probe distinte di liveness e readiness così che l'orchestratore riavvii le istanze realmente bloccate e instradi il traffico solo verso le istanze davvero pronte (dipendenze connesse, cache calde). Confondere le due cose porta o a traffico inviato a istanze non ancora pronte o a riavvii inutili di istanze che si stanno solo scaldando.
- Fai: restituisci readiness false finché la connessione al database e le dipendenze richieste non sono confermate.
- Non fare: dichiararsi healthy appena il processo si avvia, prima di poter servire davvero le richieste.