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.
Start · pause · save.
Route · distance · time.
Own + friends.
Sync later.
Pair with Delivery framework, Scaling writes, Real-time updates (when not to use WebSockets), and WhatsApp (different realtime shape).
Functional requirements
Non-functional requirements
Core entities
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).
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.
- Start →
POST /activitieswith type; service persists and returns Activity. - Pause / resume →
PATCHstate. - Save →
PATCHwithCOMPLETE.
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.
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).
-- 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.
- GPS ticks append to an in-memory buffer; UI reads locally.
- Persist buffer periodically (~10s) so a crash loses at most one window.
- On resume, reload from local storage before recording again.
- When complete + online: upload once (chunk long rides); delete local data after server ACK.
- 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.
- 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.
- 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 —
SUM(distance) GROUP BYover all activities; dies at scale. - Periodic aggregation — nightly (or hourly) job into a leaderboard table; fast reads, eventual consistency.
- Redis Sorted Sets — on COMPLETE,
ZINCRBYglobal + per-country keys;ZRANGEfor 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
Cost and performance levers
What interviewers expect by level
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.