Design problems
Platform & infrastructureMediumSDE-2 30 min

Design a real-time analytics dashboard

Asked at
BlinkitFlipkartLinkedInSwiggyZeptoZomato

The interviewer asks

“Design a live analytics dashboard for a food-delivery company. Internal ops teams watch orders per minute, GMV, cancellations and delivery times by city and zone, and 500,000 restaurant partners each see their own live numbers. Data should be at most a few seconds old, and charts must load in under a second even on IPL-final night.”

What a strong answer looks like

A strong answer splits the problem into a streaming write path and a fast read path. Apps and backend services emit events to Kafka; a stream processor (Flink) cleans, deduplicates and enriches them, handles late data with event-time windows and watermarks, and writes both raw-ish events and pre-aggregated rollups into a real-time OLAP store (Apache Pinot, ClickHouse or Druid) that can ingest from Kafka with second-level freshness and answer slice-and-dice queries in tens of milliseconds. A query service sits in front with tenant scoping (each restaurant sees only its rows), caching of hot queries for a few seconds, and push updates to open dashboards over SSE. The deep dives are choosing the OLAP store and its partitioning, event time vs processing time, approximate metrics such as distinct users with HyperLogLog, and keeping 500k tenants' queries cheap.

Concepts to mention

Kafka as the ingestion bufferStream processing with event-time windows and watermarksReal-time OLAP store (Pinot / ClickHouse / Druid)Pre-aggregated rollups with additive measuresHyperLogLog and quantile sketches for non-additive metricsPartitioning by tenant for user-facing analyticsTenant filter enforced server-sideShort-TTL query cache and SSE fan-outIdempotent upserts for replay safetyBatch reconciliation for numbers of record

1 Requirements

Functional

  • Live metrics: orders/min, GMV, average delivery time, cancellation rate, active delivery partners, sliced by city, zone, restaurant, cuisine, platform
  • Time ranges from 'last 15 minutes' (per-second resolution) to 'last 90 days' (per-hour)
  • Restaurant partners see only their own outlets' data
  • Dashboards auto-update every few seconds without manual refresh
  • Threshold alerts (e.g. cancellations in a zone above 8% for 5 minutes)
  • Drill-down from a city chart to the zones and restaurants behind a spike

Non-functional

  • Freshness: event to chart p95 under 5 s
  • Query latency p99 under 500 ms for dashboard widgets, under 2 s for ad-hoc drill-downs
  • Ingest 200k events/s at peak (app events + order lifecycle + rider pings sampled)
  • Query load: 5k internal users + up to 100k concurrent partner dashboards at dinner peak
  • Metrics within 0.5% of the finance batch numbers for GMV after the day closes; dashboards may be slightly approximate in real time
  • 99.9% availability; ingestion failure delays data but does not lose it

Out of scope (say it out loud)

  • Financial reporting of record (done by the batch warehouse)
  • A general BI tool for arbitrary SQL by analysts
  • Raw rider GPS storage

2 Estimates

Event ingest~40k/s average, ~200k/s peak~3B events/day (app interactions, order state changes, sampled rider pings) ÷ 86,400 ≈ 35k/s; dinner peak + match night ~5×
Raw storage~1.5 TB/day raw, ~200 GB/day in columnar form3B × ~500 bytes JSON; columnar compression typically 5-10×
Rollup rows~50M/dayPer minute × ~500k restaurants that had activity (~20% active in any minute) ≈ 144M max; most minutes are sparse, so ~50M rows
Dashboard query QPS~20k/s peak100k partner dashboards × ~6 widgets refreshing every 30 s ≈ 20k/s; internal dashboards add ~2k/s but are heavier
Cache effectiveness~70% hit rate on internal widgetsThousands of ops users watch the same city charts; a 2-5 s cache collapses them into one query per widget per interval

3 API

EndpointWhat it does
POST/v1/events (collector)Batched app events from clients; validated and written to Kafka
GET/v1/metrics/query{metric, filters, group_by, time_range, granularity} → time series; tenant filter injected from the auth token
SSE/v1/dashboards/{id}/streamPush updated widget values every 2-5 s to open dashboards
POST/v1/alertsCreate an alert rule {metric, filter, condition, window, channel}
GET/v1/dashboards/{id}Dashboard definition: widgets, queries, layout

4 Data model

Raw event topicsKafka
event_idevent_typeevent_timecity_idzone_idrestaurant_idorder_iduser_idamountplatformapp_version

Partitioned by order_id or user_id; schemas in a registry (Avro/Protobuf) so bad events are rejected at the collector.

orders_rt (fact table)Apache Pinot (or ClickHouse)
order_idrestaurant_idcity_idzone_idstatusplaced_atdelivered_atamountdelivery_mins

Real-time table ingesting from Kafka; upsert by order_id so the latest status wins. Partitioned on restaurant_id so partner queries hit one segment set; sorted/inverted indexes on city_id, zone_id.

minute_rollupsPinot / ClickHouse
minutecity_idzone_idrestaurant_idordersgmvcancelssum_delivery_minshll_users (sketch)

Written by Flink. Most dashboard widgets read rollups, not raw facts. Older data rolled up further to hourly.

Dashboard + alert definitionsPostgres
dashboard_idownerwidgets (query JSON)alert rulestenant scope

Small relational config.

Query cacheRedis
hash(query + tenant) → resultTTL 2-5 s for live rangeslonger for closed past ranges

Collapses identical widget queries from thousands of viewers.

Historical warehouseS3 + Iceberg (queried by Trino/Spark)
all raw eventsdaily partitions

Source for finance-grade numbers and backfills; real-time store keeps 90 days.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAbatched eventsdedup, enrich, rollupreal-time ingesthourly sinkquery / SSEtenant-scoped SQLwidget configevery 30 sApps + order servicesDashboards (ops,partners)Event collectorAlert evaluatorQuery API + SSEKafka (clean +rollups)Flink: clean, windowKafka (raw events)Pinot / ClickHouseRedis query cachePostgres (dashboards)S3 + Iceberg lake
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

An order shows up on the city chart

  1. 1Order service emits OrderPlaced (event_time = placement time) to Kafka via its outbox
  2. 2Flink deduplicates on event_id (keyed state with 1 h TTL), enriches with city and zone from a broadcast lookup table, and updates the 1-minute window for (city, zone, restaurant)
  3. 3Flink emits updated rollup rows every few seconds (early firing) and the cleaned event to output topics
  4. 4Pinot consumes the topics; rows become queryable within ~1-2 s
  5. 5The ops dashboard's SSE connection gets a refreshed value from the query API's 3-second cycle; the chart ticks up

A restaurant partner opens their dashboard

  1. 1Partner app calls /metrics/query for today's orders and GMV; the token says tenant = restaurant group 123
  2. 2Query API injects WHERE restaurant_id IN (outlets of 123), refusing any filter outside that set
  3. 3Query hits the rollup table; because the table is partitioned by restaurant_id, Pinot routes it to a few segments and answers in ~20 ms
  4. 4The response is cached for 5 s under (query, tenant) and the widget subscribes to the SSE stream for updates

Cancellation spike alert

  1. 1Alert evaluator runs every 30 s: cancellation rate per zone over the last 5 minutes
  2. 2Zone Koramangala crosses 8% for the second consecutive evaluation (simple hysteresis to avoid flapping)
  3. 3Alert posts to the ops Slack channel and PagerDuty with a link to the drill-down view

7 Deep dives

Choosing the real-time OLAP store

The read pattern is 'filter by a few dimensions, group by time and one or two dimensions, aggregate' over billions of rows, at thousands of queries per second. Row stores like Postgres cannot do this at scale; a warehouse like BigQuery or Snowflake gives seconds of latency and minutes of freshness. Real-time OLAP engines exist for exactly this: they ingest from Kafka directly, store data in columnar segments with indexes, and answer in tens of milliseconds. Apache Pinot was built at LinkedIn for user-facing analytics (every member's 'who viewed my profile'), with high QPS, star-tree pre-aggregation indexes and upserts by primary key, which suits order status changes. ClickHouse is excellent for internal analytics with rich SQL and very high scan speed, but high-QPS point-ish queries need care, and updates are done with ReplacingMergeTree or similar. Druid is similar to Pinot. For 100k partner dashboards, Pinot's partition-aware routing (partition on restaurant_id, so a partner query touches one partition) is the deciding factor; for an internal-only dashboard, ClickHouse is often simpler. Say which you pick and why, rather than listing all three.

Event time, late data and watermarks

A rider's phone in a basement sends its events 4 minutes late; Kafka consumers lag during a deploy. If we bucket events by the time we process them (processing time), a deploy creates a fake dip and then a fake spike. So windows use event time: the timestamp when the thing happened. The stream processor needs to know when a window is 'complete enough'. A watermark is its estimate that no events older than T are still coming, typically max event time seen minus an allowed lateness (say 30 s). Windows fire early results every few seconds for freshness and a final result when the watermark passes; events later than the allowed lateness are either sent to a side output and merged by an upsert, or simply corrected by the nightly batch. Clock skew on phones is real, so the collector also records server receive time and clamps absurd client times. For idempotence, rollups are written as upserts keyed by (minute, dimensions) with the full current value, not increments, so a replay after a Flink restart rewrites the same numbers instead of double-counting.

Pre-aggregation and approximate metrics

Querying raw events for every widget refresh wastes work: 'orders per minute in Bengaluru today' scans millions of rows 20k times per second. Pre-aggregate in two places. Flink writes minute rollups per (city, zone, restaurant) with additive measures: counts, sums, and sum of delivery minutes plus count (so averages can be recomputed at any level; never store an average and then average it). Pinot's star-tree index adds pre-aggregated combinations for common group-bys. Non-additive metrics need sketches. Distinct users cannot be summed across minutes or cities, so store a HyperLogLog sketch per row (~1-2% error with a few KB) and merge sketches at query time. Percentiles such as p90 delivery time use t-digest or KLL sketches, which merge correctly. Tell the interviewer which metrics are exact (orders, GMV) and which are approximate (unique users, p90), and that the finance number of record comes from the batch warehouse, reconciled daily within 0.5%.

Serving 100k tenant dashboards cheaply

Partner dashboards are many small queries, each scoped to one restaurant group. Three things keep them cheap. First, physical layout matches the access path: partition and sort by restaurant_id so a query reads a tiny slice. Second, tenant isolation is enforced in the query service, never in the client: the API takes structured queries (metric, filters, group by), not raw SQL, and always injects the tenant filter from the auth token; this also prevents a partner from guessing another restaurant's id. Third, fewer queries: dashboards subscribe over SSE, and the server computes each widget once per interval and fans the result out to all viewers of the same widget, instead of every browser polling. Cache live-range results for 2-5 s and closed past ranges (yesterday) for hours. Add per-tenant query rate limits and a query cost limit (max time range, max group-by cardinality) so one heavy drill-down cannot slow everyone. Internal ad-hoc users go to a separate replica group so their large scans do not hurt partner latency.

8 Trade-offs

Lambda vs Kappa

Kappa (one streaming path, replay from Kafka to fix bugs) keeps one codebase. Lambda adds a batch path that recomputes exact numbers. Here a mostly-Kappa design plus a nightly batch reconciliation for finance-grade metrics gives the best of both without maintaining two full pipelines.

Push (SSE) vs polling

Polling every 5 s from 100k clients is simple but multiplies queries. SSE lets the server compute once and fan out. WebSockets are unnecessary because dashboards rarely send data back.

Raw events vs rollups in the OLAP store

Rollups are fast and cheap but answer only pre-planned questions. Raw facts allow ad-hoc drill-downs but cost more per query. Keep both: rollups for dashboards, raw facts (90 days) for drill-down.

9 Bottlenecks

Hot dimension values

On match night one city dominates. Rollups are already small; the risk is a single Kafka partition if keyed by city. Key by order_id or restaurant_id, and aggregate by city in Flink with two-stage (local then global) aggregation.

Query fan-out at dinner peak

20k queries/s. Widget-level result sharing over SSE, short caches, and more Pinot broker and server replicas; pre-warm before known events like IPL matches.

Segment count and compaction

Real-time ingestion creates many small segments; schedule merges and rollups of older segments (minute → hour after 7 days) to keep query fan-out low.

10 Failure handling

Flink job restarts

Flink restores from its last checkpoint (every ~30 s) and reprocesses from the matching Kafka offsets. Rollups are upserts of full values, so reprocessing rewrites the same rows; dashboards show a short freshness gap, not wrong numbers.

OLAP store node loss

Segments have replicas (2-3); brokers route around the lost server. Real-time consumption resumes from Kafka offsets on the replacement.

Bad event schema from a new app version

Schema registry checks at the collector reject or quarantine invalid events, so a broken app release cannot poison dashboards; a metric on rejected events alerts the owning team.

11 What impresses

  • Separating internal analytics from user-facing analytics and choosing the store for each
  • Explaining why averages must be stored as sum and count
  • Using event time and explaining what a watermark is
  • Computing each widget once and fanning out over SSE
  • Being explicit about which numbers are approximate and where the exact number comes from
  • Query cost limits per tenant

12 Mistakes

  • Running dashboard queries against the production OLTP database
  • Bucketing by processing time, creating fake dips and spikes
  • Summing distinct counts across buckets
  • Writing increments to the store so replays double-count
  • Letting the client send raw SQL with its own tenant filter
  • No plan for late or duplicate events

13 Follow-ups

Q1How would you show 'live orders right now' (current state), not events per minute?
Q2How would you backfill a new metric for the last 30 days?
Q3How do you make the dashboard number match the finance report?
Q4How would you add anomaly detection instead of fixed thresholds?
Q5What changes if you need per-second resolution for the last hour?
Q6How would you handle a city-level data-residency rule?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free