Near-Real-Time Trending Dishes Dashboard

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

Design the data pipeline behind a dashboard that shows restaurant partners on our food delivery platform which dishes are trending in their city, judged on each dish's order rate over the last 30 minutes. The partner team plans for about 2 million orders a day across 300 cities, peaking near 500 orders per second at dinner.

The dish rankings update faster than the kitchen.

Business requirements

Worked solution and explanation

The trap

This is per-key anomaly detection dressed up as a leaderboard. Trending is not 'top-N orders right now'; it is 'a dish whose last 30 minutes run well above its own normal for this hour and weekday'. Anyone can draw a queue into a stream processor. What separates candidates is the baseline lookup and the partition key: sort by absolute volume and the same popular dishes trend every night, key the stream badly and one viral dish stalls trending for every city.

The default reach is a streaming job that counts orders per dish over 30 minutes and sorts by volume. Pad thai always 'trends' because it always sells; the small dish that just tripled never shows. When a social post drives one dish to many times its normal rate, a poorly keyed job lets that hot key drag every worker, and the view for every other city goes stale.


Walk the requirements

Step 1: Get order events to the trending view within minutes

Live orders land on city_dish_keyed_order_queue, which trending_scorer_30_min_rate_per_city_dish reads continuously to keep the last-30-minute rate for each city and dish pair. The scorer writes into trending_store, and partner_dashboard reads from there within a few minutes. Without a streaming scorer the view runs on batch cadence and cannot be 'right now'; without the queue, a burst that outruns the scorer either drops orders or blocks the ordering app.

Step 2: Score each dish against its own historical baseline

baseline_batch reads two years of order_history once a day and writes each dish's normal rate per hour and weekday into dish_baseline_store; the scorer looks it up on every order. The score is current_rate / baseline_rate with a noise floor so a dish going from 1 to 3 orders does not headline. A daily refresh is enough because normal drifts slowly; only the current window has to be live.

Step 3: Key by dish so one viral spike stays contained

Keying city_dish_keyed_order_queue by city and dish_id puts each dish's orders on one partition, and the 16 parallel scorer tasks each own a slice of those keys. A viral dish backs up its own partition while every other partition keeps moving, so other dishes and cities update on schedule. Key by order_id instead and every task carries a share of the hot dish's state, so the spike slows everyone.


The shape that fits

Live orders flow through a queue keyed by city and dish into a parallel streaming scorer that compares each dish's 30-minute rate with its daily-refreshed baseline; a viral dish stays on its own partition while the rest keep updating.
  • order_events (source, API)
  • order_history (source, PostgreSQL)
  • city_dish_keyed_order_queue (queue, Kafka)
  • baseline_batch (transform, Spark)
  • dish_baseline_store (storage, PostgreSQL)
  • trending_scorer_30_min_rate_per_city_dish (transform, Flink); parallelism: 16 tasks
  • trending_store (storage, PostgreSQL)
  • partner_dashboard (consumer, Grafana); freshness: < 15min
  • order_events -> city_dish_keyed_order_queue
  • city_dish_keyed_order_queue -> trending_scorer_30_min_rate_per_city_dish
  • order_history -> baseline_batch
  • baseline_batch -> dish_baseline_store
  • dish_baseline_store -> trending_scorer_30_min_rate_per_city_dish
  • trending_scorer_30_min_rate_per_city_dish -> trending_store
  • trending_store -> partner_dashboard

Common follow-up questions