Design a real-time analytics dashboard
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
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
3 API
| Endpoint | What 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}/stream | Push updated widget values every 2-5 s to open dashboards |
POST/v1/alerts | Create an alert rule {metric, filter, condition, window, channel} |
GET/v1/dashboards/{id} | Dashboard definition: widgets, queries, layout |
4 Data model
Raw event topicsKafkaevent_idevent_typeevent_timecity_idzone_idrestaurant_idorder_iduser_idamountplatformapp_versionPartitioned 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_minsReal-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 / ClickHouseminutecity_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 definitionsPostgresdashboard_idownerwidgets (query JSON)alert rulestenant scopeSmall relational config.
Query cacheRedishash(query + tenant) → resultTTL 2-5 s for live rangeslonger for closed past rangesCollapses identical widget queries from thousands of viewers.
Historical warehouseS3 + Iceberg (queried by Trino/Spark)all raw eventsdaily partitionsSource for finance-grade numbers and backfills; real-time store keeps 90 days.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
An order shows up on the city chart
- 1Order service emits OrderPlaced (event_time = placement time) to Kafka via its outbox
- 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)
- 3Flink emits updated rollup rows every few seconds (early firing) and the cleaned event to output topics
- 4Pinot consumes the topics; rows become queryable within ~1-2 s
- 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
- 1Partner app calls /metrics/query for today's orders and GMV; the token says tenant = restaurant group 123
- 2Query API injects WHERE restaurant_id IN (outlets of 123), refusing any filter outside that set
- 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
- 4The response is cached for 5 s under (query, tenant) and the widget subscribes to the SSE stream for updates
Cancellation spike alert
- 1Alert evaluator runs every 30 s: cancellation rate per zone over the last 5 minutes
- 2Zone Koramangala crosses 8% for the second consecutive evaluation (simple hysteresis to avoid flapping)
- 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
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.