A database column can be structurally valid and still be incompatible with the application processes using it. Renaming customer_name to display_name, for example, is trivial as a data-definition operation on many databases. The harder boundary appears when one application process still issues queries against the old name while another process already expects the new one.

That overlap is common whenever application replacement is not atomic. Rolling deployments, multiple service instances, delayed workers, and independent consumers can leave more than one application version active at the same time. A schema migration then has two audiences: the database engine and every executable version that can reach the database during the transition.

Expand-and-contract migration treats that compatibility interval as part of the design. The schema first expands so old and new code can coexist. Application behavior moves to the new representation. Only after obsolete code can no longer reach the database does the schema contract.

Schema validity is not application compatibility

Database migration tools usually answer whether a DDL statement can be applied. That is narrower than establishing that every active application version can continue operating after the statement commits.

Consider a direct rename:

ALTER TABLE customer
RENAME COLUMN customer_name TO display_name;

After the rename, a query that references customer_name fails because that identifier no longer exists. The database has accepted the migration, yet an older process can become incompatible immediately.

The same boundary appears with changes that do not look destructive. Adding a non-null column can conflict with old writers if the database requires a value and no default supplies one. Changing a representation from an integer code to text can leave old readers unable to interpret new values. Tightening a constraint can reject writes that an earlier application version still considers valid.

Compatibility therefore depends on operations, not just shapes. Reads, writes, constraints, defaults, triggers, generated values, and transaction behavior all contribute to the effective contract.

Expansion creates a shared interval

An expansion adds the new representation without removing the old one. For a rename, that can mean adding display_name while retaining customer_name.

ALTER TABLE customer
ADD COLUMN display_name text;

At this point, the new column does not replace anything. Both names exist, so old readers remain structurally valid. The application can then introduce code capable of working with the expanded schema.

The exact write strategy depends on the system. Some migrations use application-level dual writes. Others use a database trigger for a bounded transition. In either case, the important property is explicit: during the overlap, writes that must be visible through both representations need a defined synchronization rule.

Dual writes are not automatically atomic. Two independent statements issued outside one database transaction can leave the columns inconsistent if one succeeds and the other does not. A trigger executes under database-defined trigger and transaction semantics, but it also moves transition logic into the database. Neither mechanism is universally preferable; the required consistency boundary determines the suitable choice.

Backfill is a state transition, not a copy command

Existing rows need a value in the new representation before readers can depend on it. A backfill often looks simple:

UPDATE customer
SET display_name = customer_name
WHERE display_name IS NULL;

Its correctness depends on concurrent writes. If active code can change customer_name while the backfill runs, the migration needs an ordering model that prevents an older value from overwriting a newer one.

One valid arrangement is to establish synchronized writes before starting the backfill. The backfill then fills only rows whose new field is absent. Another arrangement can use version checks or a transaction isolation level that supplies the required conflict behavior. The relevant point is not the particular mechanism. The backfill and live-write path must agree on which value wins when they touch the same logical field.

Large tables add operational constraints. A single transaction that rewrites every row can hold locks, generate substantial transaction log volume, or retain old row versions according to the database engine and configuration. Batching can bound each transaction, but batching also means the database spends time in a mixed state. Readers introduced during that interval must tolerate rows that have and have not yet been converted.

A migration is easier to reason about when that mixed state is treated as an expected schema state rather than an accidental midpoint.

Reader migration can precede removal

Once new data is populated consistently, readers can move from the old representation to the new one. During this phase, compatibility code may read display_name and use customer_name only for records that have not yet crossed the boundary.

That fallback should have a retirement condition. If it remains indefinitely, the system preserves two representations and obscures which one is authoritative. A measurable condition is more useful than a calendar guess: the backfill has completed, new writes populate the new field, and deployed executables no longer require the old read path.

Writer migration has a similar boundary. After every active writer uses the new representation, synchronization toward the old column can stop. The old column may still exist for compatibility with remaining readers, but it is no longer part of the authoritative write path.

This separation between reader and writer migration matters because they need not converge at the same moment. Treating them as distinct transitions makes the compatibility assumptions visible.

Contraction is a compatibility assertion

Dropping the old column is not merely cleanup. It asserts that no executable still reachable in the system depends on that column.

ALTER TABLE customer
DROP COLUMN customer_name;

That assertion includes more than the primary request-serving process. Background workers, scheduled jobs, administrative commands, data exporters, and independently deployed services may all hold database access. If any of them can execute old SQL after contraction, the migration boundary is incomplete.

Rollback policy also changes the safe contraction point. If an application release may be rolled back to a version that reads the old column, removing that column also removes that rollback path. Keeping the expanded schema for an additional release interval can preserve rollback compatibility, provided the old representation remains valid enough for the older executable.

This is a temporal contract. The schema can contract only after the set of executable versions that may connect has narrowed sufficiently.

Constraints can migrate in phases too

Column presence is only one part of schema evolution. Constraints often need the same separation between introduction and enforcement.

Suppose display_name is intended to become mandatory. Adding it as nullable permits old writers to continue during expansion. After writers populate it and existing rows are backfilled, the database can enforce the stronger invariant:

ALTER TABLE customer
ALTER COLUMN display_name SET NOT NULL;

The enforcement point should follow evidence that the data satisfies the constraint and that reachable writers preserve it. Otherwise, the constraint turns an application-version mismatch into rejected writes.

Some database systems provide mechanisms for adding or validating constraints with reduced locking impact, and details vary by engine and version. Those mechanisms can change the operational cost of enforcement, but they do not remove the compatibility question. A constraint is safe only when the writers that can reach it obey the invariant it encodes.

The migration boundary belongs to the deployed system

Expand-and-contract is sometimes described as a database technique, but its central object is broader: the set of application versions and schema states that can coexist.

The expansion phase deliberately widens the database contract. The transition phase moves reads, writes, and stored data toward one representation. Contraction narrows the contract after obsolete dependencies are absent. Each phase has a distinct compatibility claim that can be inspected.

That framing also exposes cases where the pattern is unnecessary. If an application and its database can be stopped, migrated, and restarted as one atomic maintenance operation, there may be no mixed-version interval to support. If the database serves only a single process that is replaced atomically, direct changes can be adequate. Expand-and-contract earns its complexity when deployment mechanics create real overlap.

A schema change is complete only when the database shape, stored data, reachable executables, and rollback policy agree on the same contract. DDL marks part of that transition; compatibility determines its actual boundary.