Design Strava — GPS tracking, offline sync, and activity feeds

Design Strava

Fitness-tracking walkthrough: start/pause/save runs and rides, GPS routes, offline-first client recording, friends activity feed, optional live follow via polling, Redis leaderboards — with interview bars by level.

Understanding the problem

Strava is a fitness tracking app for recording and sharing activities — especially running and cycling — with analytics and a social graph. Interviewers grade whether you treat the client as part of the system (GPS, local storage, accurate mid-run UI) vs blindly streaming every coordinate to the server.

Trail notebook on device, upload when back online, friends see finished activities.
Write the trail log on-device — upload when you hit cell service.
01Record

Start · pause · save.

02Live UI

Route · distance · time.

03Feed

Own + friends.

04Offline

Sync later.

Pair with Delivery framework, Scaling writes, Real-time updates (when not to use WebSockets), and WhatsApp (different realtime shape).

Functional requirements

Functional and non-functional requirements for Strava on a whiteboard.
Availability over consistency · offline · accurate local stats · 10M concurrent.

Non-functional requirements

Core entities

User, Activity, Route, and Friend entities.
Short list is enough — align vocabulary with the interviewer.

API

Map APIs 1:1 to FRs. Prefer PATCH for partial state updates (some interviewers are REST hawks — be ready to defend PUT vs PATCH).

REST endpoints for create, patch state, routes, list, and get activity.
COMPLETE on the same PATCH path also publishes to the friends feed.
POST  /activities                         { type: RUN|RIDE } -> Activity
PATCH /activities/:id                     { state: STARTED|PAUSED|COMPLETE }
POST  /activities/:id/routes              { location }   // naive HLD only
GET   /activities?mode=USER|FRIENDS&page= -> Partial<Activity>[]
GET   /activities/:id                     -> Activity

HLD — start, pause, stop, save

Simplest client-server that meets the FR — deepen later.

Client to Activity Server to Database with Activity schema including statusUpdateEvents JSON.
Users should be able to start, pause, stop, and save their runs and rides.
  1. Start → POST /activities with type; service persists and returns Activity.
  2. Pause / resume → PATCH state.
  3. Save → PATCH with COMPLETE.

HLD — live route, distance, time

Sample GPS on a cadence (~2s ride / ~5s run). Distance via Haversine between consecutive points. Naive HLD posts each point to the server.

Client with GPS to Activity Server to Database with Activity and Route schemas.
While running or cycling, users should be able to view activity data, including route, distance, and time.

HLD — own + friends' completed activities

Completed activities are already in the DB after COMPLETE. List with mode=USER|FRIENDS; detail fetch loads the full route for a map polyline (e.g. Maps SDK).

Client with GPS to Activity Server to Database with Activity, Route, and Friend schemas.
Users should be able to view details about their own completed activities as well as the activities of their friends.
-- mode=USER
SELECT * FROM activities WHERE state='COMPLETE' AND userId=:userId
LIMIT :pageSize OFFSET ...;

-- mode=FRIENDS (bi-di friends table: two rows per friendship)
SELECT * FROM activities
WHERE state='COMPLETE'
  AND userId IN (SELECT friendId FROM friends WHERE userId=:userId)
LIMIT :pageSize OFFSET ...;

Deep dive — offline tracking

Athletes train where there's no cell. Unless you need realtime sharing, record entirely on device and sync when the activity completes (or when online). That single insight cuts server write QPS by ~100×, keeps mid-run UI accurate, and meets the offline NFR.

Client with GPS, in-memory buffer, and persistent storage syncing to Activity Server and Database.
How can we support tracking activities while offline?
  1. GPS ticks append to an in-memory buffer; UI reads locally.
  2. Persist buffer periodically (~10s) so a crash loses at most one window.
  3. On resume, reload from local storage before recording again.
  4. When complete + online: upload once (chunk long rides); delete local data after server ACK.
  5. Optional: background sync if connectivity returns mid-activity.

After offline-first, a single Activity Service (horizontally scaled) is enough — no microservice mesh required when R/W skew disappears.

Deep dive — 10M concurrent activities

With local recording, concurrency is mostly a storage and sync problem, not a GPS firehose.

Offline client, Activity Server, recent activity cache, and database sharded by completionTime.
How can we scale to support 10 million concurrent activities?
  • Napkin — ~100M activities/day → ~36.5B/year; ~600 points × ~24B ≈ ~15KB route → hundreds of TB/year of route data.
  • Shard by completion time (queries skew recent).
  • Tier — hot recent · warm months · cold years on cheaper/archive (S3).
  • Cache only if hot activity detail reads hurt — not the first lever.
  • DB brand — hot take: any major store works; data is large but not exotic.

Pattern deep dive: Scaling writes.

Deep dive — realtime sharing mid-activity

Follow-up: friends watch a live map while you're still riding. Reintroduce periodic GPS posts (2–5s) while keeping athlete-facing stats local.

Athlete posts GPS every few seconds; friends poll Activity Service with buffered lag.
Predictable cadence → friends poll · buffer 5–10s for smooth animation · WS usually overkill.
  • Updates are predictable (next point in a few seconds) — polling matches the cadence.
  • Second-level precision isn't required for friends.
  • Intentional lag / buffer makes motion look continuous instead of teleporting.
  • WebSockets/SSE + pub/sub work but add complexity you may not need — see Real-time updates.

Deep dive — leaderboards

Top athletes by distance (filter by type, country, time range).

Naive SQL aggregation versus periodic table versus Redis sorted sets.
Naive SUM → periodic agg → Redis ZINCRBY (country keys + time-range patterns).
  • Naive SQLSUM(distance) GROUP BY over all activities; dies at scale.
  • Periodic aggregation — nightly (or hourly) job into a leaderboard table; fast reads, eventual consistency.
  • Redis Sorted Sets — on COMPLETE, ZINCRBY global + per-country keys; ZRANGE for top-N.
  • Time ranges — activity IDs in a time ZSET + hash of distance/user; range query then aggregate in memory; cache hot windows.

Final design

Periodic Aggregation architecture with client buffer, recent activity cache, sharded database, and Leaderboard schema.
Periodic Aggregation — offline client · recent activity cache · shard by completionTime · Leaderboard table.

Cost and performance levers

What interviewers expect by level

Mid, senior, and staff expectations for Strava system design.
Move GPS off the server early — that's the Strava punchline.
Interview takeaway

Mid: working spine. Senior: offline-first + scale napkin. Staff: client-centric design with restrained realtime.

Wrapping up

Strava interviews reward noticing that most of the write path belongs on the athlete's phone. Once you sync on complete, 10M concurrent activities stop looking like a firehose — and you can spend depth on storage tiers, optional live follow, and leaderboards instead of inventing a WebSocket mesh for every GPS tick.

Related: Delivery framework · Scaling writes · Real-time updates · WhatsApp · LeetCode · Redis · Common patterns.

← Lattice