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

Hourly demand forecasts for every station of a bike-share network in 42 cities depend on raw data from each city's systems and from external sources, all in different formats. What pipeline turns that raw data into reliable, model-ready features?

Five cities. Five data formats. One prediction.

Business requirements

Worked solution and explanation

Many 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 and one three-year onboarding backfill must each hurt only themselves. Draw one fan-in DAG on one cluster and two things go wrong. The first late feed stalls every city's 30-minute window, and the first new-city backfill starves live predictions.

Step 1: Isolate ingestion per city under one orchestrator

per_city_ingest runs one task per city, scheduled by the orchestrator, so a missing feed fails one task and not the run. Feature compute proceeds with whichever cities arrived, and a late city falls back to its last good features, flagged stale.

Step 2: Normalize before anything computes features

normalize_common_schema turns every city and external 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 3: Check the normalized records

schema_quality_gate sits between normalization and feature_compute. That is what makes the features reliable rather than merely present: a renamed column in one city's file is caught before it turns into a zero-demand feature.

Step 4: Give backfill its own path

city_history_archive feeds backfill_job, a separate Spark pool that writes into the same feature_store. It never shares compute with the live path, so building three years of history for a new city costs the live cities nothing.

The live path ingests per city, normalizes, checks the records and computes features. Backfill rides its own pool into the same `feature_store`, which stays inside the 30-minute window at `< 15min`.
  • city_feeds (source, API)
  • external_feeds (source, API)
  • city_history_archive (source, object storage)
  • orchestrator (transform, Airflow)
  • per_city_ingest (transform, Spark); parallelism: one task per city
  • normalize_common_schema (transform, Spark)
  • schema_quality_gate (quality gate, Great Expectations)
  • feature_compute (transform, Spark)
  • backfill_job (transform, Spark); parallelism: separate backfill pool
  • feature_store (storage, warehouse); freshness: < 15min
  • demand_model (consumer)
  • city_feeds -> per_city_ingest
  • external_feeds -> per_city_ingest
  • orchestrator -> per_city_ingest
  • orchestrator -> backfill_job
  • per_city_ingest -> normalize_common_schema
  • normalize_common_schema -> schema_quality_gate
  • schema_quality_gate -> feature_compute
  • feature_compute -> feature_store
  • city_history_archive -> backfill_job
  • backfill_job -> feature_store
  • feature_store -> demand_model
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.
Per-city tasks plus a backfill pool
A late feed fails one per_city_ingest task, and the rest of the cities publish on time. The backfill pool never competes with live compute.

Common follow-up questions