Skip to content

Expand and contract

The compatibility window is the product:

old app + old schema
|
v
EXPAND <- safe while old writers and readers exist
|
v
old app + expanded schema
|
v
DEPLOY <- new app understands both schema contracts
|
v
new app + expanded schema
|
v
DRAIN <- prove old traffic is gone
|
v
CONTRACT <- validate, tighten, remove compatibility

The end state may be simple:

CREATE TABLE app.bookings (
id bigint PRIMARY KEY,
status text NOT NULL
);

Adding it as required in one operation can break old inserts. A compatible route is:

-- expand.sql
ALTER TABLE app.bookings ADD COLUMN status text;
-- application-owned backfill goes here when required
UPDATE app.bookings SET status = 'pending' WHERE status IS NULL;

Deploy code that writes and reads status, drain old writers, then enforce the contract:

-- contract.sql
ALTER TABLE app.bookings
ADD CONSTRAINT bookings_status_present
CHECK (status IS NOT NULL) NOT VALID;
ALTER TABLE app.bookings
VALIDATE CONSTRAINT bookings_status_present;
ALTER TABLE app.bookings ALTER COLUMN status SET NOT NULL;
ALTER TABLE app.bookings DROP CONSTRAINT bookings_status_present;

The generated details depend on the catalog and your decisions; the important point is that application compatibility determines phase placement.

The new application version must work immediately after expand and still work after contract. If removing the old shape requires another code release, keep the compatibility object and choose a split plan. The later feature owns its removal.

Backfills are not a third deployment phase. They are explicit work scheduled where old and new contracts remain safe. Contract should never be used as a vague container for “do risky things later.”