Thirty Cities, One Forecast

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
L5

Problem

Our operations team runs a bike-share network across dozens of cities. We want to predict hourly demand at each station so we can pre-position bikes before rush hour. The data comes from multiple city systems and external sources in different formats. Design a pipeline that takes in this raw data and produces reliable, model-ready features.

Five cities. Five data formats. One prediction.

Business requirements

Worked solution and explanation

Five formats, one clock, and a backfill that wants your cluster

This is a failure-isolation problem dressed up as a feature pipeline. Anyone can draw sources, Spark and a warehouse. What separates candidates is where the blast radius stops: one city's late feed, one city's malformed file, and a three-year onboarding backfill must each hurt only themselves. Draw one fan-in DAG on one cluster and the first late operator stalls every city's 30-minute window, while the first new-city backfill starves live predictions.

Step 1: Normalize before anything computes features

Every city and external feed lands raw in raw_landing first, untouched, so a bad parse can be replayed. normalize_common_schema then turns each format into one schema. Features computed on mixed formats are silently wrong, so normalization is its own stage and not a side effect of feature code.

Step 2: Gate the records, quarantine the bad ones

schema_quality_gate checks every normalized record. Malformed or schema-shifted rows go down an error path to dlq_quarantine while valid rows keep flowing. One city's bad file should never stop the other cities, and nothing is dropped, so someone can fix and replay it.

Step 3: Isolate per city under one orchestrator

per_city_ingest runs one task per city, so a missing feed fails one task and not the run. The orchestrator alerts by city name before the cutoff, and feature compute proceeds with whichever cities arrived. Late cities fall back to the previous window's features, flagged. Reruns are safe because ingest writes through a staging table and features upsert.

Step 4: Give backfill its own path

city_history_archive feeds backfill_job, a separate Spark pool that writes straight to the same feature_store. It never touches the live compute, so a three-year onboarding costs the live cities nothing. Overwriting by partition makes a restarted backfill harmless.

Live path normalizes, gates and computes per city; backfill rides its own pool into the same `feature_store`; the `< 15min` tiers leave room inside the 30-minute window.
  • city_feeds (source, S3)
  • external_feeds (source, API)
  • city_history_archive (source, S3)
  • orchestrator (transform, Airflow); on error: alert; alerting: City feed late vs next 30-minute window
  • per_city_ingest (transform, Spark); parallelism: one task per city; on error: retry; idempotency: staging_table
  • raw_landing (storage, S3)
  • normalize_common_schema (transform, Spark); freshness: < 15min
  • schema_quality_gate (quality gate, Great Expectations); on error: dlq
  • dlq_quarantine (storage, S3)
  • feature_compute (transform, Spark); freshness: < 15min; parallelism: 16 partitions; idempotency: upsert
  • backfill_job (transform, Spark); parallelism: separate backfill pool; backfill: partition_overwrite
  • feature_store (storage, BigQuery); freshness: < 15min
  • oncall_alerts (consumer, PagerDuty)
  • demand_model (consumer, Jupyter); freshness: < 15min
  • rebalancing_team (consumer, Tableau)
  • city_feeds -> per_city_ingest
  • external_feeds -> per_city_ingest
  • orchestrator -> per_city_ingest
  • orchestrator -> backfill_job
  • orchestrator -> oncall_alerts
  • per_city_ingest -> raw_landing
  • raw_landing -> normalize_common_schema
  • normalize_common_schema -> schema_quality_gate
  • schema_quality_gate -> feature_compute
  • schema_quality_gate -> dlq_quarantine
  • feature_compute -> feature_store
  • city_history_archive -> backfill_job
  • backfill_job -> feature_store
  • feature_store -> demand_model
  • demand_model -> rebalancing_team
  • Sources: city_feeds, external_feeds, city_history_archive
  • Live path: orchestrator, per_city_ingest, raw_landing, normalize_common_schema, schema_quality_gate, dlq_quarantine, feature_compute
  • Backfill path: backfill_job
  • Serving: feature_store, oncall_alerts, demand_model, rebalancing_team
One fan-in DAG
One late feed holds feature_compute for every city. A new-city backfill shares the cluster, so live runs stutter for hours. A single bad file can fail the whole batch.
Per-city tasks plus a backfill pool
A late feed fails one per_city_ingest task and pages by city name. The backfill pool never competes with live compute. Bad records land in dlq_quarantine and valid ones continue.

Common follow-up questions