System design sample solution

System design: aggregate metrics and analytics events

A step-by-step system design answer you could actually give in 60 minutes.

System design interview · Sample 60-minute answer · Not Stripe's actual design · Independent, not affiliated with Stripe

The system design question

Design a system that collects analytics events or metric counts from lots of services, aggregates them, and lets people query the results. Then use your data model to answer a few product questions.

Agree on the scope (5 minutes)

Say up front what you're covering.

We'll designWe'll skip
Sending events and counts from any serviceAlerting on metrics
Rolling them up into counts over timeThe dashboard UI
A query API that dashboards and people useLogs and request tracing
Answering product questions from the data modelMachine learning on the data

Then agree on what "good" means:

  • Never slows the sender: sending an event is a quick in-memory call. If our system is down, the service carries on.
  • Takes in a lot: a million events a second at peak, and more next year.
  • Fresh: a count shows up in queries within about a minute.
  • Fast to query: a dashboard covering the last week loads in a second or two.
  • Right where it matters: counts and sums are exact. Unique counts and percentiles can be close estimates, as long as we say so.

Rough numbers (5 minutes)

These are assumptions, but reasonable ones: a million events a second at peak, about 500 bytes each, and around a million distinct combinations of metric and dimensions active at once.

~1M
events per second at peak
~40 TB
of raw events a day
~1M
active metric and dimension combos
~70 GB
of minute rollups a day
QuestionRough mathWhat it means
Can we put every raw event in a database?1M a second × 500 bytesAbout 500 MB a second, or roughly 40 TB a day. Too much to index. Keep raw events in cheap files and query rollups
How big are minute rollups?1M combos × 1,440 minutes × about 50 bytesAbout 70 GB a day. A normal analytics database handles that, and hourly rollups are 60 times smaller again
Can a dashboard scan raw events?A week of raw is about 300 TBNo. Dashboards read rollups. Odd one-off questions go to a slower batch job over the raw files

The overall design (10 minutes)

Serviceslibrary batches eventsEvent streamsplit by metricAggregatorsper-minute rollupsMetrics storecolumns, by timeQuery APIdashboards, questions
Events get added up on the way in. Queries only ever read the rollups.
  1. Each service uses a small library. It records events in memory, adds up simple counts locally, and sends a batch every second or so in the background.
  2. Batches land on an event stream, split into partitions by metric name and dimensions, so every event for the same thing reaches the same aggregator.
  3. Aggregators add events up into one-minute buckets and write a row per metric, dimension combo and minute to the metrics store.
  4. A copy of the raw stream is also written to file storage as compressed files, one folder per hour, for questions the rollups can't answer.
  5. The query API reads the rollups, picks minute, hour or day rows depending on the time range, and does the final math like rates and percentiles.

What the apps need to do

ActionWho does itWhat's sent
Send eventsService library, in batchesA list of events, each with a name, time, dimensions like country or status, a value and an event ID
Define a metricEngineerName, type (count, sum, distribution, unique count), allowed dimensions and an owner
QueryDashboard or personMetric, time range, step size, filters and group-by dimensions. Gets back a table of numbers
Run a raw queryAnalystA query over the raw files. Runs as a slow batch job and returns later

What we store and where

DataWhere it goesWhy
Recent raw eventsEvent stream, kept a few daysLets aggregators replay after a crash or a bug fix
All raw eventsCompressed column files in file storage, by hourCheap, and good enough for slow one-off questions
RollupsColumn-oriented analytics database, split by timeFast sums over millions of rows, and old data is easy to drop
Metric definitionsSmall relational databaseFew rows, edited rarely, and the aggregators cache it

Step 1: Getting events in without slowing anyone down (10 minutes)

The library must never block the service. Recording an event just adds to an in-memory buffer. A background thread sends batches. If the buffer fills because we're down, it drops the oldest and counts how many it dropped, instead of eating the service's memory.

For plain counts, the library adds them up before sending: 10,000 'payment succeeded in US' events in a second become one message saying 10,000. That's the biggest single cut in volume, and it's free.

Events go onto a stream that's partitioned by a hash of metric and dimensions. That's sharding the stream, so each aggregator owns a slice and can add up in memory without talking to the others. The stream is a message queue that holds a few days, so a slow aggregator just falls behind instead of losing data.

  • Cap the dimensions on each metric. Someone adding user ID as a dimension turns one combo into millions. Reject or drop unknown dimensions, and alert the owner.
  • Each batch carries an ID, so a retried send can be spotted and skipped.
  • For crazy-high event types, sample 1 in 100 and multiply back up when querying. Label those metrics as sampled.

Step 2: Rolling events up (10 minutes)

Aggregators group events into one-minute windows by when they happened, not when they arrived. That's stream processing with event-time windows. A window stays open a couple of minutes for stragglers, then gets written out.

What you keep depends on the metric type. Counts and sums just add. For latency you can't average percentiles, so keep a histogram of bucket counts, which adds up across servers and minutes. For unique counts, like unique merchants, keep a small sketch (HyperLogLog) that merges, and accept about 1% error.

Each row has a key of metric, dimensions and minute. Writing a row is an overwrite, not an add. So if an aggregator crashes and replays from its last checkpoint, it writes the same rows again and nothing gets counted twice. Events that arrive really late get added as a correction to the minute they belong to.

Rolling up again over time

A background job turns minute rows into hour rows, and hour rows into day rows. Keep minutes for a month, hours for a year and days forever. A query for last quarter then reads about 2,000 hour rows per combo instead of 130,000 minute rows.

Step 3: The data model and the product questions (10 minutes)

The rollup table has one row per metric, dimension combo and time bucket. The columns are the dimensions (like country, payment method and status), plus count, sum, a histogram and a unique-count sketch. A column-oriented database is a good fit here, because queries add up a few columns over many rows. The SQL vs NoSQL question really comes down to that read pattern.

Say the interviewer asks 'what's the payment success rate by country this week?'. Query 'payments attempted' grouped by country and status from the hourly table, then divide succeeded by total. Always divide totals. Averaging each hour's rate gives quiet hours the same weight as busy ones.

'What's p99 checkout latency per endpoint?' merges the histograms for each endpoint, then reads the 99th percentile off the merged one. 'How many distinct merchants used a feature yesterday?' merges the daily sketches. If they want it exact, that's a batch query over the raw files.

Questions about one specific merchant are the hard case, because merchant ID has far too many values to be a normal dimension. Build a separate daily table keyed by merchant for the handful of metrics people ask about that way, and say so when you hit it.

Choices you should be able to defend

We choseInstead ofBecauseWe'd reconsider if
Store minute rollupsStore and scan every raw eventThousands of times less data, and fast dashboardsMost questions needed individual events
Add up counts in the librarySend every event as it happensCuts volume a lot before it leaves the hostWe needed each event's exact time and details
Sketches for unique countsExact sets of IDsTiny and mergeable across time and serversBilling or anything where 1% off isn't OK
Drop events when buffers fillBlock the service until we accept themOur outage never becomes a payments outageThe events were needed for money, not metrics
Overwrite rows by keyAdd to rows as events arriveReplays don't double countWe had no clean checkpoints to replay from

What could go wrong (5 minutes)

What happensWhat we do about it
The event stream is downLibraries buffer, then drop the oldest and count what they dropped. Services keep running
An aggregator crashesAnother picks up its partitions and replays from the last checkpoint, overwriting the same rows
Someone adds a dimension with millions of valuesThe dimension limit rejects it and alerts the metric's owner
Events arrive an hour lateThey're added as a correction to the right minute, and the dashboard shows the fixed number
A bug made a day's rollups wrongFix the bug and rebuild that day from the raw files

Follow-up questions (5 minutes)

How would you add alerts?

Run alert rules against the minute rollups as they're written, so alerts see the same numbers dashboards do. Keep it separate from the query path so heavy dashboards can't delay an alert.

What if someone wants a new dimension on old data?

Rollups only have the dimensions they were built with. You'd rebuild from the raw files for the period they care about, which is a slow batch job but doable.

What about multiple regions?

Roll up in each region, then send the much smaller rollups to one place for global queries. Raw events stay in the region they came from.

How the 60 minutes add up

MinutesWhat you're doing
0–5Agree on what you're building
5–10Rough numbers
10–20Draw the overall design
20–30Getting events in without slowing anyone down
30–40Rolling events up
40–50The data model and the product questions
50–55What could go wrong
55–60Follow-up questions

Key concepts used here

Further reading

We wrote this sample answer ourselves.

Practice Loop is independent and isn't affiliated with or endorsed by Stripe.