Scopri come i sistemi costruiti con AI gestiscono i cambiamenti di schema in modo sicuro: versioning, rollout retrocompatibili, migrazioni dati, test, osservabilità e strategie di rollback.

Uno schema è semplicemente l'accordo condiviso sulla forma dei dati e cosa significa ogni campo. Nei sistemi costruiti con AI questo accordo appare in più posti rispetto alle sole tabelle del database — e cambia più spesso di quanto i team si aspettino.
Incontrerai schemi in almeno quattro livelli comuni:
Se due parti del sistema si scambiano dati, esiste uno schema — anche se nessuno lo ha scritto.
Il codice generato dall'AI può accelerare drasticamente lo sviluppo, ma aumenta anche il churn:
id vs. userId) emergono quando si generano o refactorizzano componenti in modo distribuito.Il risultato è una “drift” del contratto tra producer e consumer più frequente.
Se usi un workflow basato su generazione (per esempio generare handler, layer di accesso DB e integrazioni tramite chat), vale la pena integrare la disciplina degli schemi in quel workflow fin dal primo giorno. Piattaforme come Koder.ai aiutano i team a muoversi rapidamente generando app React/Go/PostgreSQL e Flutter da un'interfaccia chat — ma più veloce è il rilascio, più importante diventa versionare le interfacce, validare i payload e distribuire le modifiche con attenzione.
Questo post si concentra su modi pratici per mantenere stabile la produzione pur iterando velocemente: mantenere la retrocompatibilità, distribuire le modifiche in sicurezza e migrare i dati senza sorprese.
Non approfondiremo modellazione teorica, metodi formali o funzionalità specifiche di vendor. L'enfasi è su pattern applicabili trasversalmente — che il vostro sistema sia scritto a mano, assistito dall'AI o per lo più generato dall'AI.
Il codice generato dall'AI tende a far sembrare i cambiamenti di schema "normali" — non perché i team siano negligenti, ma perché gli input al sistema cambiano più frequentemente. Quando il comportamento dell'app è parzialmente guidato da prompt, versioni di modelli e codice "collante" generato, la forma dei dati è più soggetta a deriva nel tempo.
Alcuni schemi ricorrono frequentemente e generano churn:
risk_score, explanation, source_url) o dividere un concetto in più campi (es. address in street, city, postal_code).Il codice generato dall'AI spesso “funziona” rapidamente, ma può codificare assunzioni fragili:
La generazione di codice incoraggia l'iterazione rapida: rigeneri handler, parser e layer di accesso al DB mentre i requisiti evolvono. Questa velocità è utile, ma rende anche facile rilasciare piccoli cambiamenti d'interfaccia ripetutamente — a volte senza accorgersene.
La mentalità più sicura è trattare ogni schema come un contratto: tabelle del database, payload API, eventi e anche risposte strutturate dall'LLM. Se un consumer dipende da qualcosa, versionalo, validalo e cambialo deliberatamente.
I cambiamenti di schema non sono tutti uguali. La prima domanda utile è: i consumer esistenti continueranno a funzionare senza modifiche? Se sì, di solito è additivo. Se no, è breaking — e serve un piano di rollout coordinato.
I cambiamenti additivi estendono ciò che c'è già senza cambiarne il significato.
Esempi comuni su database:
preferred_language).Esempi non-database:
Gli additivi sono “sicuri” solo se i consumer più vecchi sono tolleranti: devono ignorare campi sconosciuti e non richiedere i nuovi.
I cambiamenti breaking alterano o rimuovono qualcosa da cui i consumer già dipendono.
Tipici cambiamenti breaking su database:
Cambiamenti breaking non-database:
Prima di fare merge, documenta:
Questa breve “nota d'impatto” obbliga alla chiarezza — specialmente quando il codice generato dall'AI introduce cambiamenti di schema implicitamente.
Versionare è il modo per comunicare ad altri sistemi (e al te stesso futuro) “questo è cambiato, e qui c'è il rischio”. Lo scopo non è la burocrazia: è prevenire rotture silenziose quando client, servizi o pipeline aggiornano a velocità diverse.
Pensa in termini major / minor / patch, anche se non pubblichi letteralmente 1.2.3:
Una regola semplice che salva i team: non cambiare mai silenziosamente il significato di un campo esistente. Se status="active" significava "cliente pagante", non riutilizzarlo per significare "l'account esiste". Aggiungi un nuovo campo o una nuova versione.
Di solito hai due opzioni pratiche:
1) Endpoint versionati (es. /api/v1/orders e /api/v2/orders):
Buono quando i cambiamenti sono veramente breaking o diffusi. È chiaro, ma può creare duplicazione e manutenzione a lungo termine se mantieni più versioni.
2) Campi versionati / evoluzione additiva (es. aggiungi new_field, mantieni old_field):
Buono quando puoi fare cambiamenti additivi. I client più vecchi ignorano ciò che non capiscono; i client nuovi leggono il nuovo campo. Col tempo, depreca e rimuovi il campo vecchio con un piano esplicito.
Per stream, queue e webhook, i consumer sono spesso fuori dal tuo controllo di deployment. Un schema registry (o qualsiasi catalogo centrale di schemi con controlli di compatibilità) aiuta a far rispettare regole come “solo cambiamenti additivi ammessi” e rende ovvio quali producer e consumer dipendono da quali versioni.
Il modo più sicuro per rilasciare cambiamenti di schema — specialmente quando hai più servizi, job e componenti generati dall'AI — è il pattern expand → backfill → switch → contract. Minimizza downtime ed evita deploy "tutto o niente" dove un consumer in ritardo rompe la produzione.
1) Expand: Introduci il nuovo schema in modo backward-compatible. Lettori e scrittori esistenti devono continuare a funzionare senza cambiamenti.
2) Backfill: Popola i nuovi campi per i dati storici (o reprocessa i messaggi) così il sistema diventa consistente.
3) Switch: Aggiorna writers e readers per usare il nuovo campo/formato. Questo può essere fatto gradualmente (canary, rollout percentuale) perché lo schema supporta entrambi.
4) Contract: Rimuovi il campo/formato vecchio solo dopo esserti assicurato che nulla dipenda più da esso.
Rollout in due fasi (expand → switch) o tre fasi (expand → backfill → switch) riduce downtime perché evita accoppiamenti stretti: gli scrittori possono muoversi prima, i lettori dopo, e viceversa.
Supponiamo di voler aggiungere customer_tier.
customer_tier come nullable con default NULL.customer_tier, e aggiorna i reader per preferirlo.Tratta ogni schema come un contratto tra producer (scrittori) e consumer (lettori). Nei sistemi costruiti con AI questo è facile da perdere di vista perché compaiono nuovi percorsi di codice rapidamente. Rendi i rollout espliciti: documenta quale versione scrive cosa, quali servizi possono leggere entrambi e la precisa “data del contratto” in cui i campi vecchi possono essere rimossi.
Le migrazioni di database sono il "manuale d'istruzioni" per muovere dati e struttura di produzione da uno stato sicuro al successivo. Nei sistemi costruiti con AI contano ancora di più perché il codice generato può presumere che una colonna esista, rinominare campi in modo incoerente o cambiare vincoli senza considerare le righe esistenti.
File di migrazione (committati nel source control) sono passi espliciti come “add column X”, “create index Y” o “copy data from A to B”. Sono auditabili, revisionabili e possono essere riprodotti in staging e produzione.
Auto-migrations (generate da un ORM/framework) sono comode per sviluppo iniziale e prototipazione, ma possono produrre operazioni rischiose (dropping di colonne, ricostruzione di tabelle) o riordinare i cambiamenti in modi non intenzionati.
Una regola pratica: usa le auto-migrations per bozzare i cambiamenti, poi trasformale in file di migrazione revisionati per qualsiasi cosa tocchi la produzione.
Rendi le migrazioni idempotenti dove possibile: rieseguirle non dovrebbe corrompere i dati o fallire a metà. Preferisci "create if not exists", aggiungi nuove colonne come nullable prima, e proteggi le trasformazioni di dati con check.
Mantieni anche un ordine chiaro. Ogni ambiente (locale, CI, staging, prod) dovrebbe applicare la stessa sequenza di migrazioni. Non “fixare” la produzione con SQL manuale a meno che non catturi la modifica in una migrazione dopo.
Alcuni cambiamenti di schema possono bloccare le scritture (o anche le letture) se bloccano una tabella grande. Modi di alto livello per ridurre il rischio:
Per database multi-tenant, esegui migrazioni in un loop controllato per tenant, con tracking del progresso e retry sicuri. Per shard, tratta ogni shard come un sistema di produzione separato: rilascia migrazioni shard-per-shard, verifica la salute, poi procedi. Questo limita il raggio d'azione e rende il rollback fattibile.
Un backfill è quando popoli nuovi campi (o correggi valori) per record esistenti. Il reprocessamento è quando fai passare i dati storici di nuovo in una pipeline — tipicamente perché le regole di business sono cambiate, è stato risolto un bug o l'output del modello è stato aggiornato.
Entrambi sono comuni dopo cambiamenti di schema: è facile iniziare a scrivere la nuova forma per i "nuovi dati", ma i sistemi di produzione dipendono anche dai dati di ieri coerenti.
Backfill online (in produzione, gradualmente). Esegui un job controllato che aggiorna i record in piccoli batch mentre il sistema resta live. È più sicuro per servizi critici perché puoi limitare il carico, mettere in pausa e riprendere.
Backfill batch (offline o job schedulati). Processi grandi chunk durante finestre di bassa attività. È operativamente più semplice, ma può creare picchi di carico e richiede più tempo per recuperare da errori.
Backfill pigro alla lettura. Quando si legge un record vecchio, l'app calcola/popola i campi mancanti e li scrive. Questo diluisce il costo nel tempo ed evita un grande job, ma rallenta la prima lettura e può lasciare dati "vecchi" non convertiti a lungo.
In pratica i team combinano spesso questi approcci: backfill pigro per la coda lunga e job online per i dati più frequentemente accessi.
La validazione deve essere esplicita e misurabile:
Valida anche gli effetti a valle: dashboard, indici di ricerca, cache ed eventuali esportazioni che dipendono dai campi aggiornati.
I backfill bilanciano velocità (completare in fretta) contro rischio e costo (carico, compute e overhead operativo). Definisci i criteri di accettazione in anticipo: cosa significa “fatto”, runtime previsto, massimo tasso di errore accettabile e cosa fare se la validazione fallisce (pausa, retry o rollback).
Gli schemi non vivono solo nei database. Ogni volta che un sistema invia dati a un altro — topic Kafka, code SQS/RabbitMQ, payload webhook, anche “eventi” scritti su object storage — hai creato un contratto. Producer e consumer si muovono indipendentemente, quindi questi contratti tendono a rompersi più spesso rispetto alle tabelle interne di una singola app.
Per stream di eventi e payload webhook, preferisci cambiamenti che i vecchi consumer possano ignorare e che i nuovi consumer possano adottare.
Una regola pratica: aggiungi campi, non rimuovere o rinominare. Se devi deprecare qualcosa, continua a inviarlo per un po' e documentalo come deprecato.
Esempio: estendi un evento OrderCreated aggiungendo campi opzionali.
{
"event_type": "OrderCreated",
"order_id": "o_123",
"created_at": "2025-12-01T10:00:00Z",
"currency": "USD",
"discount_code": "WELCOME10"
}
I consumer più vecchi leggono order_id e created_at e ignorano il resto.
Invece di far indovinare al producer cosa possa rompere gli altri, i consumer pubblicano ciò da cui dipendono (campi, tipi, regole required/optional). Il producer poi valida i cambiamenti rispetto a quelle aspettative prima di rilasciare. Questo è particolarmente utile in codebase generate dall'AI, dove un modello potrebbe "utilemente" rinominare un campo o cambiare un tipo.
Rendi i parser tolleranti:
Quando serve un breaking change, usa un nuovo tipo di evento o un nome versione (per esempio OrderCreated.v2) ed esegui entrambi in parallelo finché tutti i consumer non migrano.
Quando aggiungi un LLM a un sistema, i suoi output diventano rapidamente uno schema de facto — anche se nessuno ha scritto una specifica formale. Il codice a valle comincia a presumere "ci sarà un campo summary", "la prima riga è il titolo" o "i bullet sono separati da trattini". Quelle assunzioni si consolidano nel tempo, e un piccolo cambiamento nel comportamento del modello può romperle proprio come un rename di colonna.
Invece di parsare "testo carino", richiedi output strutturati (tipicamente JSON) e validali prima che entrino nel resto del sistema. Consideralo come la trasformazione da "best effort" a contratto.
Un approccio pratico:
Questo è particolarmente importante quando le risposte degli LLM alimentano pipeline di dati, automazioni o contenuti rivolti all'utente.
Anche con lo stesso prompt, gli output possono cambiare nel tempo: campi possono essere omessi, chiavi extra possono apparire e i tipi possono cambiare ("42" vs 42, array vs stringa). Tratta questi eventi come evoluzioni di schema.
Mitigazioni efficaci:
Un prompt è un'interfaccia. Se lo modifichi, versionalo. Mantieni prompt_v1, prompt_v2 e rilascia gradualmente (feature flag, canary o toggles per tenant). Testa con un set di valutazione fisso prima di promuovere i cambiamenti e tieni le versioni più vecchie in esecuzione finché i consumer a valle non si sono adattati. Per maggiori dettagli sulle meccaniche dei rollout sicuri, collega il tuo approccio a /blog/safe-rollouts-expand-contract.
I cambiamenti di schema falliscono spesso in modi banali ma costosi: una nuova colonna manca in un ambiente, un consumer si aspetta ancora un campo vecchio, o una migrazione funziona su dati vuoti ma va in timeout in produzione. I test sono il modo per trasformare queste “sorprese” in lavoro prevedibile e riparabile.
Unit test proteggono la logica locale: funzioni di mapping, serializer/deserializer, validator e query builder. Se un campo viene rinominato o un tipo cambia, gli unit test dovrebbero fallire vicino al codice che va aggiornato.
Integration test assicurano che la tua app funzioni ancora con dipendenze reali: il motore di database reale, lo strumento di migrazione reale e i formati dei messaggi reali. Qui catturi problemi come “il modello ORM è cambiato ma la migrazione no” o “il nuovo nome dell'indice confligge”.
End-to-end test simulano outcome utente o workflow tra servizi: crea dati, migrali, leggili via API e verifica che i consumer a valle si comportino ancora correttamente.
L'evoluzione degli schemi spesso rompe ai confini: API service-to-service, stream, queue e webhook. Aggiungi contract test che girino da entrambe le parti:
Testa le migrazioni come le deployeresti:
Conserva un piccolo set di fixture che rappresentino:
Queste fixture rendono regressioni evidenti, specialmente quando il codice generato dall'AI cambia sottilmente nomi di campo, optionalità o formattazione.
I cambiamenti di schema raramente falliscono in modo rumoroso al momento del deploy. Più spesso la rottura si manifesta come un lento aumento di errori di parsing, avvisi “campo sconosciuto”, dati mancanti o job di background che restano indietro. Una buona osservabilità trasforma quei segnali deboli in feedback azionabili mentre puoi ancora mettere in pausa il rollout.
Inizia dalle basi (salute dell'app), poi aggiungi segnali specifici per lo schema:
La chiave è comparare prima vs dopo e scomporre per versione client, versione schema e segmento di traffico (canary vs stable).
Crea due viste di dashboard:
Dashboard comportamento applicazione
Dashboard migrazioni e job background
Se esegui un rollout expand/contract, includi un pannello che mostri letture/scritture separate per schema vecchio vs nuovo così puoi vedere quando è sicuro passare alla fase successiva.
Fai partire pagine su problemi che indicano perdita o lettura errata dei dati:
Evita alert rumorosi su 500 grezzi senza contesto; collega gli alert al rollout dello schema usando tag come versione schema ed endpoint.
Durante la transizione, includi e logga:
X-Schema-Version, campo metadata nel messaggio)Quel dettaglio rende la risposta alla domanda “perché questo payload ha fallito?” risolvibile in minuti, non giorni — specialmente quando servizi diversi (o versioni modello diverse) sono live contemporaneamente.
I cambiamenti di schema falliscono in due modi: il cambiamento stesso è sbagliato, o il sistema attorno ad esso si comporta diversamente dal previsto (soprattutto quando il codice generato dall'AI introduce assunzioni sottili). In entrambi i casi, ogni migrazione necessita di una storia di rollback prima di essere rilasciata — anche se quella storia è esplicitamente “nessun rollback”.
Scegliere “nessun rollback” può essere valido quando il cambiamento è irreversibile (per esempio droppare colonne, riscrivere identificatori o deduplicare record in modo distruttivo). Ma “nessun rollback” non significa assenza di piano; è una decisione che sposta il piano verso fix forward, restore e contenimento.
Feature flag / config gate: Incapsula nuovi reader, writer e campi API dietro un flag così puoi spegnere il nuovo comportamento senza ridistribuire. Questo è particolarmente utile quando codice generato dall'AI è sintatticamente corretto ma semanticamente sbagliato.
Disable dual-write: Se scrivi su schema vecchio e nuovo durante un rollout expand/contract, tieni uno switch per disattivare. Spegnere la nuova scrittura arresta ulteriori divergenze mentre indaghi.
Revert dei reader (non solo writer): Molti incidenti succedono perché i consumer iniziano a leggere campi o tabelle nuove troppo presto. Rendi semplice puntare i servizi alla versione precedente dello schema o ignorare i campi nuovi.
Alcune migrazioni non possono essere annullate pulitamente:
Per questi, pianifica restore da backup, replay da eventi o ricomputazione dagli input grezzi — e verifica di avere ancora quegli input.
Un buon change management rende rari i rollback — e rende il recovery noioso quando accade.
Se il tuo team itera rapidamente con sviluppo assistito dall'AI, aiuta accoppiare queste pratiche con tool che supportino sperimentazione sicura. Ad esempio, Koder.ai include la modalità di planning per la progettazione anticipata delle modifiche e snapshot/rollback per un recupero rapido quando una modifica generata accidentalmente sposta un contratto. Usati insieme, generazione rapida di codice e disciplina nell'evoluzione degli schemi ti permettono di muoverti più veloce senza trattare la produzione come un ambiente di test.