A New Column on a Billion Rows

A hard Pipeline Design interview practice problem on DataDriven. Design the pipeline on the canvas: sources, transforms, storage and checks, then get feedback on it.

Domain
Pipeline Design
Difficulty
hard
Seniority
L6

Problem

Our orders table holds a billion rows and takes 50K writes a minute, and both analytics replicas and the change-data-capture feed that loads our warehouse read from it. Design how we add a new column and fill it for every existing row within a week, knowing the next app release starts setting the column on every write.

Add and backfill a new column to a billion-row production table with zero downtime.

Business requirements

Worked solution and explanation

The trap: it's the default value

This is a zero-downtime schema evolution problem wearing a "product wants a column" costume. The real skill is to split the shape change from the value change, so that neither one ever hits the write path at full weight. Anyone can type ALTER TABLE orders ADD COLUMN. The trap is the default. On many engines, ADD COLUMN ... DEFAULT x rewrites all billion rows under a lock while 50K writes a minute queue behind it. A full-speed backfill afterwards then floods replication for everything that reads this table. Get either one wrong and on-call kills your migration on day one.

The statement only says that availability can't slip. Before drawing anything, a senior turns that sentence into four questions. What replicates from this table? Will the app write the new column while the backfill runs? How far behind can replicas fall? How fast must we be able to back out? Here the answers are: analytics replicas and a warehouse CDC feed, yes, an agreed lag budget, and within a week. The design answers all four.


Six moves, in order

Step 1: Add the column as NULL, with no default

The schema_change_plan ships a nullable column. On modern engines that is a catalog edit, not a table rewrite, so the latency budget never notices it. Every value arrives later, through a path you control.

Step 2: Turn on the dual write

Next, the app starts setting new_column on every insert and update, through the dual_write_layer. From that moment the set of rows that need backfilling only shrinks. It also means that two writers now touch the same column, and the next two moves exist because of that.

Step 3: Backfill in key ranges, paced by lag

The orchestrator hands batched_backfill one primary-key range at a time. After each chunk it records the range's end in backfill_checkpoint and reads replication_lag_check. Over budget means pause; recovered means resume from the checkpoint. A crash means restart from the checkpoint, not from row one.

Step 4: Write only where new_column IS NULL

Every chunk passes through fill_if_empty_guard, which runs UPDATE ... WHERE id BETWEEN :lo AND :hi AND new_column IS NULL. If the app got to a row first, the backfill skips it. That one predicate is both the overwrite protection and the idempotency.

Step 5: Validate before you cut over

The cutover_validator checks three things: the count of rows where new_column IS NULL has reached zero, a sample of app-written values is unchanged, and the replicas have caught up. Only after that do you add NOT NULL or point readers at the column.

Step 6: Rehearse the rollback first

The rollback_plan drops the column (a metadata change) and clears the checkpoint state. rollback_rehearsal runs it on a staging copy before the backfill starts, so the runtime is a measured number rather than a hope.


The reference design

Metadata-only add. Key-range chunks resume from `backfill_checkpoint`, pause on `replication_lag_check`, and write only where `new_column IS NULL`. Cutover is validated and the rollback is rehearsed.
  • schema_change_plan (transform, SQL)
  • dual_write_layer (source, API); freshness: real-time
  • orders_oltp (source, PostgreSQL)
  • orchestrator (transform, Airflow); on error: alert; alerting: Lag past budget or chunk failed
  • batched_backfill (transform, SQL); parallelism: 4 key ranges; on error: retry; retries: 3; backoff: exponential
  • fill_if_empty_guard (quality gate, custom)
  • backfill_checkpoint (storage, PostgreSQL)
  • cdc_capture (source, CDC); on error: alert
  • replication_lag_check (quality gate, custom)
  • replication_target (storage, PostgreSQL)
  • warehouse_loader (transform, Spark); freshness: < 1h
  • analytics_warehouse (storage, warehouse); freshness: < 1h
  • cutover_validator (quality gate, custom)
  • rollback_rehearsal (quality gate, custom)
  • rollback_plan (transform, SQL)
  • schema_change_plan -> orders_oltp
  • schema_change_plan -> orchestrator
  • dual_write_layer -> orders_oltp
  • orchestrator -> batched_backfill
  • batched_backfill -> fill_if_empty_guard
  • fill_if_empty_guard -> orders_oltp
  • batched_backfill -> backfill_checkpoint
  • backfill_checkpoint -> orchestrator
  • orders_oltp -> cdc_capture
  • cdc_capture -> replication_target
  • cdc_capture -> warehouse_loader
  • warehouse_loader -> analytics_warehouse
  • cdc_capture -> replication_lag_check
  • replication_lag_check -> orchestrator
  • orders_oltp -> cutover_validator
  • rollback_rehearsal -> rollback_plan
  • orchestrator -> rollback_plan
  • rollback_plan -> orders_oltp
  • Schema change and rollback: schema_change_plan, rollback_rehearsal, rollback_plan
  • Live writes: dual_write_layer, orders_oltp
  • Backfill: orchestrator, batched_backfill, fill_if_empty_guard, backfill_checkpoint, replication_lag_check
  • Replication: cdc_capture, replication_target, warehouse_loader, analytics_warehouse
  • Cutover: cutover_validator
Upsert backfill
UPDATE orders SET new_column = :derived across the chunk, or ON CONFLICT DO UPDATE. A row the app set ten seconds ago gets replaced by the backfill's stale derivation. Reruns converge, so it looks idempotent, but every rerun converges on the wrong value.
Fill-only-if-empty backfill
The same UPDATE, plus AND new_column IS NULL. The app's value always wins, rerunning a finished chunk is a no-op, and the guard costs nothing extra because it filters rows the key range already fetched.

Common follow-up questions