Schema and API evolution discipline: backwards-compatible changes, rolling deploy safety, transition periods, deprecation-over-removal, and consumer impact assessment. Corrects destructive renames, required field additions, atomic switchover assumptions, and missing runtime deprecation warnings. Use when changing database schemas, API contracts, config formats, shared struct definitions, environment variables, or CLI interfaces. Triggers: migration, rename, schema, breaking change, backwards compatible, deprecate, column, field, ALTER, evolution, rolling deploy, config change, env var rename, version, transition.
Installation
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Schema and API evolution discipline: backwards-compatible changes, rolling deploy safety, transition periods, deprecation-over-removal, and consumer impact assessment. Corrects destructive renames, required field additions, atomic switchover assumptions, and missing runtime deprecation warnings. Use when changing database schemas, API contracts, config formats, shared struct definitions, environment variables, or CLI interfaces. Triggers: migration, rename, schema, breaking change, backwards compatible, deprecate, column, field, ALTER, evolution, rolling deploy, config change, env var rename, version, transition.
Evolution — What Claude Gets Wrong
You refactor like the old version vanishes the instant you deploy. It doesn't. Rolling deploys mean old code reads new data (and vice versa) for minutes or hours. Multi-binary systems mean one binary updates while others run the old version. Config files in the wild don't update themselves.
The Iron Rule
Every change to a shared interface must be safe for both old and new consumers simultaneously. Shared interfaces include: database columns, serialized messages (JSON, protobuf, job queue payloads), API response shapes, config file fields, environment variables, CLI flags, and public function signatures in shared libraries.
Anti-Patterns You Default To
Anti-pattern
Example
Fix
Destructive rename
ALTER TABLE RENAME COLUMN
Add new column → backfill → dual-write → migrate consumers → drop old
Required field on existing data
model: String (no default)
#[serde(default)] model: Option<String> or DEFAULT in migration
Atomic switchover
Rename env var everywhere at once
New code reads new name, falls back to old, warns
No deprecation warning
Old config field silently ignored
tracing::warn!("interval_ms is deprecated, use interval_secs")
Missing consumer enumeration
Update struct, miss inline SQL/SSE types/dashboard
List ALL consumers before changing
Enum variant removal
Remove "active" status, old data breaks
Keep old variants, map to new internally
Breaking return type change
Vec<T> → HashMap<K, T> on public fn
Add new method, deprecate old
The Three-Phase Pattern
Every breaking change follows three phases:
Phase 1: Expand (add new, keep old)
-- Database: add new column alongside oldALTER TABLE users ADDCOLUMN email_address TEXT;
UPDATE users SET email_address = email;
-- Code: write to BOTH columns
// Config: accept both field names#[derive(Deserialize)]structConfig {
interval_secs: Option<u64>,
#[serde(rename = "interval_ms")]
legacy_interval_ms: Option<u64>,
}
implConfig {
fninterval(&self) -> Duration {
ifletSome(ms) = self.legacy_interval_ms {
tracing::warn!("'interval_ms' is deprecated, use 'interval_secs'");
Duration::from_millis(ms)
} else {
Duration::from_secs(self.interval_secs.unwrap_or(60))
}
}
}
Phase 2: Migrate (move consumers to new)
Update all consumers to use the new interface
Old interface still works but warns
Phase 3: Contract (remove old)
Only after ALL consumers confirmed migrated
Remove old column/field/function
This is a SEPARATE deployment from Phase 1
New Fields on Existing Data
// WRONG: existing serialized jobs lack this field — deserialization fails#[derive(Deserialize)]structInferenceJob {
prompt: String,
model: String, // Old jobs in queue don't have this
}
// RIGHT: default for backwards compatibility#[derive(Deserialize)]structInferenceJob {
prompt: String,
#[serde(default = "default_model")]
model: String,
}
fndefault_model() ->String { "auto".into() }
-- WRONG: NOT NULL without default on table with existing rowsALTER TABLE jobs ADDCOLUMN model TEXT NOT NULL;
-- RIGHT: default covers existing rowsALTER TABLE jobs ADDCOLUMN model TEXT NOT NULLDEFAULT'auto';
Environment Variables & CLI Flags
// WRONG: rename and hope all deploy manifests update simultaneouslyletdb = env::var("MONOLITH_DB_URL")?;
// RIGHT: fallback chain with warningletdb = env::var("MONOLITH_DB_URL").or_else(|_| {
letval = env::var("DATABASE_URL")?;
tracing::warn!("DATABASE_URL is deprecated, use MONOLITH_DB_URL");
Ok(val)
})?;