Database migrations in production are the riskiest part of any deployment. The expand-contract pattern ensures zero-downtime schema changes.
The Expand-Contract Pattern
Phase 1: EXPAND — Add new column/table alongside old one
Phase 2: MIGRATE — Backfill data, update app to write to both
Phase 3: CONTRACT — Remove old column/table after full cutoverExample: Renaming a Column Safely
// WRONG: Breaks running application
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('name', 'full_name');
});
// RIGHT Phase 1: Add new column
Schema::table('users', function (Blueprint $table) {
$table->string('full_name')->nullable();
});
// Phase 2: Backfill data
DB::statement('UPDATE users SET full_name = name WHERE full_name IS NULL');
// Phase 3: After code is updated, drop old column
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('name');
});Safe Migration Practices
// Always add columns as nullable or with defaults
$table->string('timezone')->default('UTC');
$table->string('avatar_url')->nullable();
// Use batched updates for large tables
User::whereNull('full_name')->chunkById(1000, function ($users) {
foreach ($users as $user) {
$user->update(['full_name' => $user->name]);
}
});Rules for Production Migrations
- Never rename or drop columns in the same deploy as code changes
- Always make schema changes backward-compatible
- Test migrations against a copy of production data
- Set statement timeouts to prevent long-running locks
- Always write a
down()method for rollback