Risposta diretta
Le migration Flyway applicate sono immutabili per contratto, non per suggerimento: cambiare anche un solo commento in un file già applicato in produzione fa fallire il boot con un mismatch di checksum. L'ho rotto io stesso una volta, con una modifica cosmetica. Su 189 migration in produzione, quella è stata la lezione più costosa: un editor di testo può crashare un backend Spring Boot più velocemente di un bug applicativo.
La disciplina sulle migration di database è la parte più noiosa e più sottovalutata dell'ingegneria backend, finché non ti crasha la produzione. Su questo portfolio, 189 migration Flyway (fino a V199) governano lo schema PostgreSQL di un backend con oltre 3.100 classi Java. In tre incidenti di produzione distinti, ognuno riconducibile a una violazione della stessa regola implicita "non toccare mai qualcosa che il sistema considera già scritto e verificato", ho imparato quanto sia stretto il margine tra disciplina noiosa e boot crash in produzione.
Postmortem 1: un commento ha rotto il checksum
Flyway calcola un checksum del contenuto testuale di ogni file di
migration già applicato, e lo confronta ad ogni avvio contro il valore
salvato nella tabella flyway_schema_history. Se non
combaciano, Flyway rifiuta di avviare l'applicazione. Nel 2026-06-20
ho modificato un singolo commento SQL in una migration già applicata
(V152), pensando che un commento non potesse avere effetti funzionali,
per chiarire un'intenzione poco chiara a distanza di mesi. Il backend
non è ripartito: FlywayException: Migration checksum mismatch.
Nessuna colonna, nessuna tabella, nessuna logica era cambiata: solo
testo dopo un doppio trattino --. Flyway non distingue
tra commento e statement, tratta il file come un blob di byte con un
checksum, punto.
La regola operativa che applico ora, senza eccezioni: una migration applicata è congelata per sempre, incluso ogni commento. Se serve chiarire un'intenzione, si scrive una nuova migration che documenta il perché nel messaggio di commit o in un commento nella nuova migration, mai modificando la vecchia.
Postmortem 2: la race del deploy JAR
Il deploy manuale del backend segue lo schema git pull +
mvn package + systemctl restart. Prima
della correzione, lo script copiava il nuovo JAR sopra quello in uso
(cp) mentre la JVM precedente era ancora in esecuzione e
teneva il file mappato in memoria. Il risultato osservato in
produzione: NoClassDefFoundError su classi core di Spring,
perché il classloader stava leggendo un file parzialmente sovrascritto
a metà di una cp non atomica, mentre il vecchio processo
era ancora vivo e serviva traffico. Il sintomo è particolarmente
insidioso perché non è un errore di compilazione né un errore
applicativo, è un file binario corrotto a runtime che sembra un bug
di dipendenze mancanti.
Il fix ha tre parti, e tutte e tre sono necessarie, nessuna da sola
basta: fermare il servizio prima di copiare (non dopo),
copiare il nuovo JAR con una mv atomica invece di un
cp in-place, e resettare lo stato failed di
systemd se il precedente restart era andato storto, prima di riavviare.
Saltare uno qualunque dei tre passi riproduce la race in modo
intermittente, che è il tipo di bug più costoso da diagnosticare
perché non si ripresenta ad ogni deploy.
Postmortem 3: due bean, stesso nome semplice, boot crash
Due classi @Service in package Java diversi ma con lo
stesso simple name (es. ExportService in due verticali di
business separati) fanno esplodere il boot Spring con
ConflictingBeanDefinitionException, perché Spring, senza
un nome esplicito, deriva il nome del bean dal simple class name, non
dal fully qualified name. Il punto davvero scomodo: i test unitari
non catturano questo problema, perché nessun singolo test carica
l'intero application context con entrambi i verticali insieme, solo
un test di integrazione full-context lo rivela, e nella pratica quel
test full-context viene eseguito raramente durante lo sviluppo di
routine, spesso solo prima del deploy o mai.
Con oltre 25 verticali di business separati aggiunti nel tempo, il
rischio di collisione sul simple name cresce combinatoriamente, non
linearmente, con il numero di verticali. La regola che ho adottato:
ogni nuovo @Service in un vertical business specifico
riceve un nome esplicito con prefisso (@Service("phExportService")),
non lasciato al default, e prima di ogni deploy gira almeno un test
di integrazione full-context.
| Incidente | Sintomo osservato | Regola anti-ripetizione |
|---|---|---|
| Modifica commento migration applicata (V152) | Boot crash, FlywayException checksum mismatch |
Migration applicate immutabili, sempre, anche per un commento |
| Race deploy JAR in-place | NoClassDefFoundError intermittente |
Stop-prima-di-copiare + mv atomica + reset-failed |
| Bean duplicati stesso simple name | ConflictingBeanDefinitionException al boot |
Nome bean esplicito con prefisso vertical + test full-context pre-deploy |
Il pattern comune: l'immutabilità come confine, non come stile
I tre postmortem sembrano indipendenti, ma condividono la stessa causa radice: qualcosa che il sistema tratta come già scritto, verificato, immutabile, viene modificato o sovrascritto in un momento in cui il sistema non se lo aspetta. Una migration applicata, un binario in esecuzione, un simple name che Spring assume univoco. L'immutabilità non è uno stile di programmazione elegante da seguire quando conviene, è un confine di sistema che, se violato, produce un crash deterministico o una race intermittente, mai un warning gentile.
Dal postmortem alla strada asfaltata: come una piattaforma trasforma la regola in un guardrail
In un articolo di platform engineering la domanda non è «che cosa ho imparato» ma «che cosa deve rendere impossibile la piattaforma». Una regola scritta in un postmortem vale finché qualcuno la ricorda; un guardrail vale anche quando non la ricorda nessuno. Ecco i tre incidenti convertiti, con il costo di ciascuna conversione.
- Migration già applicata. Guardrail: un controllo prima del merge che fallisce se un file già presente in
db/migrationrisulta modificato rispetto alla versione base. Il repo ha oltre duecento migration, quindi il controllo è economico (un confronto di file) e il suo costo è l'attrito: una correzione legittima richiede un'azione deliberata (una nuova migration o unflyway repairconsapevole) invece di una modifica distratta. - Sostituzione del JAR. Guardrail: lo script di deploy ferma il servizio, copia in un file temporaneo e rinomina in modo atomico, e prende un lock esclusivo. Il deploy del frontend di questo sito ne ha già uno (un file di lock in
/var/run): un secondo deploy contemporaneo muore con «Altro deploy in corso». Costo: un deploy interrotto lascia il lock, e va rilasciato mandando SIGTERM al processo, che lo libera da sé. - Bean con lo stesso nome. Guardrail: un test che avvia il contesto o, più leggero, scansiona le classi annotate e fallisce se due hanno lo stesso nome semplice. Costo: un test in più, che deve restare veloce, altrimenti viene escluso dalla suite.
I tre guardrail hanno la stessa forma: un controllo automatico che sposta l'errore dal momento del guasto al momento della modifica, quando costa un minuto invece di un'interruzione. Si distinguono per il prezzo in attrito. Il controllo sulle migration costa poco ma ti obbliga a una scelta consapevole quando la correzione è legittima: per riallineare i checksum nella tabella dello schema esiste il comando flyway repair, che la documentazione descrive come riallineamento dei checksum delle migration applicate con quelle disponibili, da usare sapendo perché.
La sostituzione atomica del file regge per una ragione precisa: una mv è una rename, che POSIX richiede atomica, e un processo che ha il vecchio file aperto continua a leggerlo anche dopo la sostituzione; vale però solo nello stesso file system, perché fra file system diversi rename fallisce con EXDEV. Per i nomi dei bean, infine, Spring documenta un'alternativa ai prefissi scritti a mano: un generatore di nomi che usa il nome completo della classe, FullyQualifiedAnnotationBeanNameGenerator, con il costo di cambiare il nome di tutti i bean e di rompere ogni riferimento per nome.
Il guardrail è anch'esso un componente che può morire
Il quarto incidente della raccolta (il test di architettura che eseguiva zero test) dice che ogni guardrail ha bisogno di una prova di vita: un caso che deve fallire apposta, o un controllo sul numero di test eseguiti. Costruire strade asfaltate senza questa prova produce falsa sicurezza, che è peggio dell'assenza dichiarata di sicurezza. Il quadro completo, comprese le sei trappole di processo di ottobre 2026, è nell'articolo di approfondimento Quattro postmortem di produzione.
Quando questa disciplina è overkill
Su un side project a persona singola con un solo ambiente e restart manuale poco frequente, il rischio di race sul deploy JAR è basso e il costo di uno script di deploy sofisticato con stop-prima-di-copiare non si giustifica. Il punto di rottura reale è la produzione con traffico continuo: lì anche una finestra di race di pochi secondi diventa una finestra di errore visibile agli utenti, e la disciplina smette di essere burocrazia e diventa protezione necessaria.
Che cosa sai fare adesso
Sai trasformare un postmortem in un controllo che gira prima del guasto, e sai quale prezzo in attrito paga ciascuno dei tre. Sai perché una sostituzione di file si fa con mv nello stesso file system e non con cp, e quale decisione implica flyway repair. L'articolo precedente, il piano di implementazione di una IDP, ha messo in fila le fasi; il successivo, AI on-prem contro cloud, chiude la serie con una decisione di architettura.
Domande frequenti
Perché modificare un commento in una migration Flyway già applicata rompe il boot?
Flyway calcola un checksum sul contenuto testuale completo di ogni migration già applicata e lo confronta ad ogni avvio con il valore salvato nella tabella flyway_schema_history. Non distingue tra commento e statement SQL: qualunque modifica al testo, anche cosmetica, cambia il checksum e fa fallire l'avvio con un mismatch.
Cosa causa un NoClassDefFoundError durante il deploy di un JAR Spring Boot?
Se il nuovo JAR viene copiato in-place sopra quello in uso mentre la JVM precedente è ancora in esecuzione e ha il file mappato in memoria, il classloader può leggere un file parzialmente sovrascritto a metà di una copia non atomica. Il sintomo appare come una dipendenza mancante, ma la causa reale è un file binario corrotto a runtime durante il deploy.
Come si evita la race condition nel deploy di un JAR Spring Boot?
Fermando il servizio prima di copiare il nuovo JAR (non dopo), usando una mv atomica invece di una cp in-place, e resettando lo stato failed di systemd se un restart precedente era andato storto. Tutti e tre i passi sono necessari insieme; saltarne uno riproduce la race in modo intermittente.
Perché due bean Spring con lo stesso simple name fanno crashare il boot?
Senza un nome esplicito, Spring deriva il nome di un bean @Service dal simple class name, non dal fully qualified name. Due classi in package diversi ma con lo stesso simple name generano una ConflictingBeanDefinitionException al boot, un problema che i test unitari isolati non catturano perché nessun singolo test carica entrambi i verticali nello stesso application context.
Cosa hanno in comune i tre postmortem di produzione descritti qui?
Condividono la stessa causa radice: qualcosa che il sistema tratta come già scritto, verificato o immutabile (una migration applicata, un binario in esecuzione, un simple name assunto univoco) viene modificato o sovrascritto in un momento in cui il sistema non se lo aspetta, producendo un crash deterministico o una race intermittente.
Nella stessa serie trovi anche l'introduzione alla piattaforma interna e l'automazione nelle piattaforme.
Fonti e approfondimenti
- Flyway - comando validate - il checksum (CRC32 per le migration SQL) salvato all'esecuzione e confrontato dopo; consultata il 2026-11-13
- Flyway - migration versionate - buona pratica di non modificare le migration già applicate e creare una nuova migration; consultata il 2026-11-13
- Flyway - comando repair - riallineamento dei checksum delle migration applicate; consultata il 2026-11-13
- Spring Framework - Classpath scanning, nomi dei componenti - nome predefinito del bean (nome della classe senza package, con l'iniziale minuscola) e generatore con nome completo; consultata il 2026-11-13
- POSIX - rename() - azione atomica, errore EXDEV fra file system diversi, file aperto che resta leggibile; consultata il 2026-11-13
Commenti
Caricamento commenti...
Accedi per commentare