Question Breakdowns

Activity Tracker

A worked system design: ingest time-stamped activity from millions of devices (think Strava/fitness) and serve summaries and history. Time-series storage, write-heavy ingest, and rollups.


An activity tracker ingests streams of time-stamped data from many devices — GPS points, heart rate, steps — and lets users see their activities, history, and aggregated stats. The defining characteristic is the data shape: a relentless, append-only flood of time-stamped measurements, queried mostly by time range. That shape points straight at Time-Series Databases.

1. Requirements

Functional:

  • Devices upload activity data points (timestamp + metrics, often GPS).
  • Users view individual activities and historical trends/summaries.
  • Aggregate stats: weekly distance, average pace, personal records.

Non-functional:

  • Write-heavy ingest — millions of devices, each emitting frequent points.
  • Time-range reads — "this run," "last 30 days" — never random access.
  • Durable — a user's history must persist.
  • Scale — huge volume, append-only, old data rarely changes.

2. The data is time-series

Each data point is (device/user, timestamp, metrics), written once, never updated, and read by time window. This is the textbook time-series profile, and it's worth naming explicitly because it dictates the storage choice:

points ( user_id, activity_id, ts, lat, lng, heart_rate, ... )   -- append-only, time-ordered

A general-purpose row store can do this, but a time-series database (or a wide-column store partitioned by (user, time-bucket)) is built for it: time-ordered storage, automatic partitioning by time, compression of sequential points, and efficient range scans.

3. Ingest: absorb the firehose

Devices upload frequently, often in batches (a watch syncs a whole run at once), and connectivity is spotty. So:

  • Accept uploads through a thin endpoint that appends to a durable buffer (Kafka or directly to the time-series store) — don't do heavy work on the request.
  • Expect batched, late, out-of-order points (a watch offline mid-run syncs later); the store must accept points for past timestamps, and summaries recompute when late data lands.
  • Writes dominate, so this is a Scaling Writes problem — partition by user so each user's series is independent and the write load spreads.

4. Reads: raw vs. rollups

Two very different queries:

  • View one activity: a time-range scan of one activity's points — fast and natural in a time-series store (contiguous, time-ordered).
  • Trends and totals ("weekly distance for a year"): scanning every raw point is wasteful. Pre-aggregate into rollups — downsample raw points into per-activity and per-day summaries as data arrives (Time-Series Databases continuous aggregates). A "last year" chart then reads ~365 daily rows, not millions of points.
raw points ──roll up──► per-activity summary ──roll up──► daily/weekly aggregates
fine detail (one run)                            cheap long-range trends

5. Storage lifecycle

Old raw points are rarely viewed at full resolution. Use a retention + downsampling policy: keep raw points hot for recent activities, then downsample and tier older data to cheaper storage, keeping the summaries. This is standard for time-series and controls the otherwise unbounded storage growth.

6. Deep dives

  • GPS specifics: a route is a sequence of points; simplify (e.g. Douglas–Peucker) for display and distance without storing every raw fix forever.
  • Personal records / leaderboards: computed from rollups; leaderboards are a Top-K-style aggregation.
  • Idempotent uploads: a re-synced batch must not double-count — dedup on a point/activity id.
  • Real-time view: a live activity (tracking a run in progress) streams points via a real-time channel in addition to the durable write.

What to emphasise

Name the time-series shape and the raw-vs-rollup split:

"The data is classic time-series — append-only, time-stamped points read by time range — so I'd store it in a time-series database or a wide-column store partitioned by user and time bucket, which gives time-ordered storage, compression, and efficient range scans. Ingest is write-heavy and bursty with late, out-of-order batches from devices syncing after being offline, so a thin endpoint appends to a durable buffer and I partition by user to spread writes. For reads I separate viewing one activity — a time-range scan — from trends, which I serve from pre-computed daily/weekly rollups so a year-long chart reads hundreds of rows, not millions. Old raw data downsamples and tiers to cheap storage on a retention policy."


Next up: Distributed Cache — building the cache itself.