Expand, migrate, contract, across three releases

A column rename is three changes, not one, and each of them leaves the application working with both the old and the new shape.

// release 1 — expand: add the column, write both, read the old
Schema::table('orders', function (Blueprint $t) {
    $t->unsignedBigInteger('customer_id')->nullable()->after('customer_ref');
});

// release 2 — migrate: backfill in batches, then read the new one
// release 3 — contract: stop writing the old one, then drop it

Nothing in that sequence takes a lock for longer than adding a nullable column, which is instant in 8.0. The cost is three deploys and a period where two columns hold the same value, and the discipline is in actually doing the third — a codebase that has adopted this and never contracts accumulates duplicated columns nobody remembers the reason for. Scheduling the contraction as part of the same piece of work is what prevents that.