•CASE STUDY

Fitness Tracking App (Strava-style)

4 min read·690 words·Intermediate

Asked at

1 candidate report in Mar 2026

How to use this case study

SDE-2 / Mid

  • Design APIs to upload workouts (with GPS routes)
  • Store them
  • Show history and stats; plus following friends and an activity feed

SDE-3 / Senior

  • Go deeper on storing routes (compressed polylines, object storage)
  • Processing uploads asynchronously
  • Offline uploads from devices
  • Computing stats

Staff / Principal

  • Discuss segments and leaderboards (matching routes to segments)
  • Privacy zones
  • Scaling the feed
  • Costs of storing high-frequency sensor data

Problem RestatementProblem

Meta asked: design the backend of a fitness tracking app. Users record workouts (runs, rides, walks) with a phone or watch: GPS route, time, distance, pace, heart rate. They see their history and statistics (weekly distance, personal records), follow friends and see an activity feed, and optionally compete on segments (popular stretches of road with leaderboards).

RequirementsRequirements

  • Upload workouts (maybe recorded offline) with GPS points every second and sensor data.
  • Show a workout: map, splits, charts. Show history and totals.
  • Social: follow, feed, kudos and comments.
  • Segments and leaderboards (optional).
  • Privacy: hide the start and end near home (privacy zones), and private activities.

1.1 Scale

  • 50M users, 5M workouts/day. A 1-hour workout = 3,600 GPS points ≈ 50–100 KB compressed → ~500 GB/day of route data.

ArchitectureArchitecture

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    DEV["Phone / watch - records offline"] -->|"upload file (FIT/GPX/JSON)"| API["Upload API"]
    API --> OS[("Object storage - raw files")]
    API --> Q[("Processing queue")]
    Q --> PROC["Processor - clean, stats, privacy zones"]
    PROC --> DB[("Activities DB - summary")]
    PROC --> RT[("Route store - encoded polyline")]
    PROC --> SEG["Segment matcher"]
    SEG --> LB[("Leaderboards")]
    PROC --> K[("Activity events")]
    K --> FEED["Feed fan-out"]
    FEED --> FS[("Feed store")]
    APP["App"] --> RAPI["Read APIs"]
    RAPI --> DB
    RAPI --> RT
    RAPI --> FS

Data ModelData model

activities:  activity_id, user_id, type, start_time, duration_s, distance_m, elevation_m,
             avg_pace, avg_hr, visibility, summary_polyline, created_at
routes:      activity_id → full-resolution encoded polyline + sensor streams (object storage / blob)
stats:       user_id, week, distance_m, time_s, count      (pre-aggregated)
follows:     follower_id, followee_id
  • Summary data (small) goes in the DB for lists and feeds. Full streams (big) go in object storage, loaded only when the user opens the activity.
  • Routes are stored as an encoded polyline (a compact text encoding of coordinates), plus a simplified version for thumbnails.

Key FlowsFlows

  1. Upload: the device records locally (works offline) and uploads the file when connected, with an idempotency key (the device's activity UUID) so retries don't duplicate. The API stores the raw file and queues processing, then returns right away.
  2. Processing: parse, smooth GPS noise, compute distance, pace, splits and elevation, apply privacy zones (trim points near home), save the summary and streams, update weekly stats, and match segments.
  3. Feed: publish "activity created". Fan out to followers' feeds (on write for normal users, on read for accounts with huge followings).

Deep Dive — Matching a run against every segmentDeep dive

A user finishes a 10 km run. The app has to work out which of millions of user-created segments they just covered, and where they place on each leaderboard.

Weak

Compare the activity against every segment

For each segment, check whether the route covers it.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  ACT["New activity - 3,000 GPS points"] --> LOOP["Loop over 8,000,000 segments"]
  LOOP --> GEO["Polyline comparison per segment"]
  GEO --> COST["Billions of point comparisons per upload"]
  COST --> NEVER["Minutes of CPU for one run, x every upload"]

The work is the product of activities and segments, and almost all of it compares a run in Amsterdam against segments in Colorado. The geometry is the expensive part and it is being spent on pairs that a bounding box would have rejected instantly.

Good

Find candidate segments by location

Index each segment's bounding box in a geospatial index, and query it with the activity's bounding box. Now only segments physically near the route are considered — a handful instead of millions.

The right filter, and it is only a filter. A bounding box overlap does not mean the runner covered the segment: they may have crossed it perpendicularly, run it backwards, or passed its start and end on different streets. Accepting every candidate produces leaderboard entries for runs that never happened.

Best

Cheap candidates, then an ordered traversal check

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  ACT["Activity route"] --> BB["Geospatial index - segments whose bbox overlaps"]
  BB --> CAND["Candidate segments - tens, not millions"]
  CAND --> START{"Route passes near the segment start?"}
  START -->|"no"| DROP["Discard"]
  START -->|"yes"| END{"Passes near the end, later in time?"}
  END -->|"no"| DROP
  END -->|"yes"| PATH{"Route stays close to the polyline between them?"}
  PATH -->|"yes"| EFF["Compute elapsed time"]
  EFF --> LB[("Sorted set per segment - overall, age group, friends")]
  • Order matters, and so does time. The route must pass the start before the end. Checking proximity alone matches people who ran it in reverse.
  • Verify the middle, not just the endpoints. Requiring the route to stay within a tolerance of the polyline rejects the runner who took a different street between the same two corners.
  • Leaderboards are sorted sets keyed by segment, one per view — overall, by age group, among friends. Inserting one entry updates all the ranks for free, which is what makes "you're 412th" instantly answerable.

Do the matching asynchronously after the upload. The user wants their run saved immediately; segment results appearing a few seconds later is normal and expected, and it keeps a slow match out of the upload path.

Wrap-UpWrap-up

Devices record offline and upload whole activity files idempotently. An async processor cleans GPS, computes stats, applies privacy zones, stores small summaries in the DB and full streams as encoded polylines in object storage, and updates pre-aggregated weekly stats. Activity events drive feed fan-out (hybrid for popular users), and a geospatial segment matcher updates sorted-set leaderboards.

More Case Studies

Frequently Asked Questions

What is the Fitness Tracking App (Strava-style) system design question?

Fitness Tracking App (Strava-style) is a system design interview question asked at FAANG companies. It covers geospatial, storage, api design, real-time and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Fitness Tracking App (Strava-style) question?

Meta have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Fitness Tracking App (Strava-style) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Fitness Tracking App (Strava-style) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →