Skip to content
Frontend system design ยท about 40 minutes

Live analytics dashboard

Design an analytics dashboard for a business product. The page has about ten widgets: number cards, line and bar charts, and a table. The user filters by date range, region and product, and every widget follows the filters. The key numbers update every few seconds. Users keep the page open for hours, often on a large screen. A view with its filters must be shareable as a link.

Ask first

  • How fresh must the data be: one second, five seconds, a minute? Is it the same for every widget?
  • How many points can one chart have: hundreds, or hundreds of thousands?
  • Do all widgets share the same filters, or do some have their own?
  • Is the layout fixed, or can users move widgets and save views?
  • Can the server push updates, or must the client ask for them?
  • Do we need export, and must the charts be usable with a screen reader?

The outline

Write your own outline for each part first, then open it to compare with the points a strong answer covers.

Requirements
  • Functional: show the widgets, apply shared filters, update the live numbers, share a view as a link
  • Each widget loads, fails and retries alone; one slow query does not block the page
  • After a filter change, no widget shows data for the old filters as if it were new
  • The page stays responsive during updates, and memory does not grow over hours
  • The user can see how fresh each number is
  • Out of scope unless asked: writing a chart library, the query engine on the server
Architecture
  • The dashboard page owns the filters and the layout; each widget is independent and asks for its own data with the current filters
  • Filters live in the URL query string; the page reads them from the URL, and each widget builds its request from them
  • A query cache, keyed by widget and filters, removes duplicate requests and keeps data when a widget mounts again
  • Live data: polling per widget with its own interval is the simple choice; one SSE or WebSocket stream for the page fits when the server can push
  • Each widget sits in an error boundary and has its own loading and error state
  • Charts use SVG for small series and canvas for large ones
Data model
  • Filters: from and to (or a relative range such as the last 24 hours), region, product; written to the URL
  • Widget config: id, type, title, metric, refresh interval, position in the grid
  • Series: a list of { t, value } points; the server groups the data into the time bucket that the chart needs
  • Query state per widget: data, status, error, updatedAt; old data stays visible while new data loads
  • Totals and percent change are computed from the data and are not stored
Interface
  • GET /metrics/:metric?from=&to=&region=&product=&bucket=1m returns { points: [{ t, value }], updatedAt }
  • A batch call, POST /metrics/query with a list of widget queries, cuts the number of requests on the first load
  • Live: GET /stream?metrics=a,b with the same filters as an event stream of { metric, t, value }, or the normal GET on an interval
  • Widget props: config and filters; a chart or a number card does not read the URL itself
  • FilterBar props: value and onChange; the page writes the new value to the URL
  • Times are ISO 8601 in UTC; the client formats them in the user's time zone
Optimizations
  • Polling stops when the tab is hidden (Page Visibility API), and one fetch runs when the tab is visible again
  • A filter change cancels the running requests, and a response for old filters is ignored
  • The server reduces a series to about one point per pixel; a chart 1,000 pixels wide does not need 100,000 points
  • Live updates are batched and painted at most once per animation frame, and only the widget with new data renders again
  • Widgets below the visible area, and the chart library itself, load lazily
  • Accessibility: each chart has a text summary and a data table; color is not the only signal; live numbers are not announced on every update
  • Errors: a failed widget keeps its last data with a mark that it is old, shows a retry, and backs off if the server keeps failing

Pitfalls

  • One request for the whole page, so the slowest query blocks every widget
  • Filters only in component state, so a link or a reload loses the view
  • Polling for hours in a hidden tab
  • Rendering every widget again on each update
  • Sending raw rows to the browser and grouping them there

Follow-ups an interviewer asks

  • Polling, SSE or WebSocket: which one here, and what would change your choice?
  • The user changes a filter three times in one second. Which requests are sent, and what is shown?
  • One chart must show 500,000 points. What do you do?
  • How do you make a line chart usable for a blind user?
  • After eight hours the tab uses 2 GB of memory. Where do you look first?
  • How does the user know that a number on the screen is old?