# 7. The backfill window bug [← 6. The NOT NULL trap](06-the-not-null-trap.md) · [Next: 8. The rolling window proof →](08-the-rolling-window-proof.md) Deploy 1's migration backfills `email_address` for every row that exists *at the moment it runs*. It cannot see rows written after that — and a rolling deploy is not instantaneous, so there's a real window, between "Deploy 1's migration finished" and "every replica in the fleet is confirmed Stage 2 or later", during which a still-live Stage 1 instance keeps inserting rows the old way: `email` only, `email_address` untouched, `NULL`. That window isn't a hypothetical edge case — it's guaranteed to happen for however long Deploy 2's rollout takes, on every real fleet bigger than one instance. [`BackfillWindowBugTest`](https://ankurm.com/git.app/asmhatre/spring-boot-demo/src/branch/main/db-migrations-expand-contract/src/test/java/com/ankurm/expandcontract/BackfillWindowBugTest.java) writes exactly such a row, then reads it back two ways: ``` -- the row a lingering Stage 1 instance just wrote -- [NAME=Katherine Johnson, EMAIL=katherine@example.test, EMAIL_ADDRESS=null] -- a NAIVE Stage 3 read (email_address alone) - the bug -- naive Stage 3 email column value: null -- the SHIPPED Stage 3 read (CustomerService, COALESCE) - the fix -- CustomerService (stage 3) result: Optional[Customer[id=1, name=Katherine Johnson, email=katherine@example.test]] ``` Full transcript: [`docs/output/07-backfill-window-bug.txt`](https://ankurm.com/git.app/asmhatre/spring-boot-demo/src/branch/main/db-migrations-expand-contract/docs/output/07-backfill-window-bug.txt). The naive read — `SELECT email_address FROM customers WHERE id = ?` — is exactly what you'd write if you thought of the read switch as "just point at the new column now that it's backfilled". It's wrong specifically for rows born during Deploy 2's own rollout, because those rows are younger than Deploy 1's one-time backfill and were written by an instance that (correctly, for its stage) never touched `email_address`. The fix is the `COALESCE(email_address, email)` already shown in [chapter 5](05-the-read-switch.md). It costs nothing for a fully dual-written row — `COALESCE` returns the first non-null argument, and both columns already agree — and it's the only thing that returns a correct answer for a row that hasn't caught up yet. ## Going deeper - Stage 2 never needs this fallback: it still reads the original `email` column, which every stage keeps populated the entire time. Only Stage 3, the first stage to prefer the new column, needs the fallback — see the read switch in [chapter 5](05-the-read-switch.md). - This is the database-level version of a general rolling-deploy rule: **any code path that reads data written by a different stage has to tolerate that stage's write shape**, not just the shape your own stage would have produced. [← 6. The NOT NULL trap](06-the-not-null-trap.md) · [Next: 8. The rolling window proof →](08-the-rolling-window-proof.md)