Design StrideSync: A Scalable Platform

Design a Strava-scale fitness platform with capacity math and trade-offs.

Introduction

30 Second Summary

Every app you use daily has an architecture behind it. When millions of people upload runs, scroll their feeds, and tap "like" at the same time, someone had to design the system that keeps it all running.

In this project, you will design the architecture for StrideSync, a social fitness platform handling 10M daily active users. You'll produce a structured design document with capacity math, architecture diagrams, and justified trade-offs.

What You'll Build

You'll open your finished Markdown file and see rendered Mermaid architecture diagrams showing data flowing from GPS uploads through Kafka event pipelines to Redis-cached follower feeds, backed by capacity math that proves every design choice.

By the end of this project, you'll have:

  • Capacity math you can walk through in an interview, proving your system handles 35K peak feed reads/sec and 2.4M GPS points/sec with numbers that justify every technology choice.
  • Rendered architecture diagrams showing microservice boundaries, async pipelines, and a polyglot data layer you can explain end-to-end.
  • A hybrid fan-out feed system design you can defend with concrete trade-off analysis, showing exactly where fan-out-on-write breaks and why a pull-based fallback is necessary.
  • Secret Mission: Redesign the entire system for 100M DAU with geographic sharding, multi-region deployment, and hot-key mitigation strategies.

Are there any prerequisites?

This project assumes familiarity with databases (PostgreSQL, Redis), message queues (Kafka), and basic cloud concepts (object storage, load balancers). No prior system design interview experience is required.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters.

You're about to design a distributed system that handles millions of GPS uploads, delivers real-time social feeds, and scales horizontally.

Set Up Your Environment

System design is a visual discipline. Every architecture decision you make needs to live in a diagram you can iterate on, share, and defend in an interview.

In this step, you'll set up VS Code with a Mermaid diagram extension so you can write architecture diagrams as text and see them rendered live. This gives you instant visual feedback as you build your design document.

In this step, get ready to:
  • Install the Mermaid Preview extension in VS Code.
  • Create the project directory and starter document.
  • Verify Mermaid diagrams render in the live preview.
Install the Mermaid Preview extension

Mermaid is a diagramming language that turns plain text into flowcharts, sequence diagrams, and architecture diagrams. VS Code's built-in Markdown preview doesn't render Mermaid by default, so you need an extension to see your diagrams.

  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Markdown Preview Mermaid Support into the search bar.
  • Find the extension by Matt Bierner (publisher: bierner) and click Install.
  • Reload VS Code if prompted.

Don't see the extension?

Make sure you're searching the full name Markdown Preview Mermaid Support. If you see multiple results, look for the one by publisher bierner. It should have a high download count.

help me find and install the Mermaid extension in VS Code

Create the document structure

Your design document follows the structured methodology used in system design interviews: requirements, estimation, architecture, API, data layer, async processing, and bottleneck analysis. Each section builds on the previous one.

  • Open the integrated terminal in VS Code by pressing Control+` (macOS) or Ctrl+` (Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir stridesync-design && cd stridesync-design
  • Create the design document file by running:
touch DESIGN.md
  • Confirm the file exists by running:
ls

You should see DESIGN.md listed in the output.

  • Open the stridesync-design folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Select the stridesync-design folder on your Desktop, then click Open.
  • Click on DESIGN.md in the Explorer sidebar to open it in the editor.
  • Paste the following starter template into DESIGN.md:
# StrideSync System Design

## 1. Requirements

### Functional Requirements
(to be filled)

### Non-Functional Requirements
(to be filled)

## 2. Capacity Estimation
(to be filled)

## 3. High-Level Architecture
(to be filled)

## 4. API Design
(to be filled)

## 5. Data Layer
(to be filled)

## 6. Feed System & Async Pipeline
(to be filled)

## 7. Bottleneck Analysis & Monitoring
(to be filled)

What are these sections?

This template mirrors the structure interviewers expect in a system design answer. You start with requirements to scope the problem, then use capacity math to derive constraints, then build the architecture on top of those constraints.

Each section builds on the previous one. Your capacity numbers in Section 2 will directly dictate your technology choices in Sections 5 and 6.

  • Save DESIGN.md by pressing Cmd+S (macOS) or Ctrl+S (Windows).

You should see the seven section headings visible in your editor with (to be filled) placeholders beneath each one.

File looks empty or won't save?

Make sure you opened the stridesync-design folder (not just the file) via File > Open Folder. If you see a blank file, confirm you pasted the template into the correct file by checking the tab title says DESIGN.md.

help me open my DESIGN.md file in VS Code

Verify Mermaid rendering

The extension you installed adds Mermaid support to VS Code's built-in Markdown preview. To confirm it works, you'll add a simple architecture diagram to your file and check that it renders as a visual flowchart.

  • Add this test diagram to the bottom of DESIGN.md (below the last section heading):
```mermaid
graph LR
    A[Mobile Client] --> B[API Gateway]
    B --> C[Activity Service]
    C --> D[(PostgreSQL)]
    style A fill:#e1f5fe
    style D fill:#fff3e0
```

What does this diagram describe?

This is a simplified version of the architecture you'll fully design later. graph LR tells Mermaid to draw a left-to-right flowchart. Each node is labeled (like A[Mobile Client]) and arrows show data flow between them.

The style lines add color to specific nodes. Blue for the client, orange for the database. These visual cues make architecture diagrams scannable at a glance.

  • Save DESIGN.md.

Before you open the preview, what do you expect to see? A rendered diagram with colored shapes and arrows, or the raw Mermaid text?

  • Open the Markdown preview side-by-side by pressing Cmd+K then V (macOS) or Ctrl+K then V (Windows).

You should see a rendered flowchart with a blue "Mobile Client" node connecting through "API Gateway" to "Activity Service" to an orange "PostgreSQL" node.

✔️ I see a rendered diagram

Your Mermaid extension is working. You'll use this preview throughout the project to visualize every architecture decision as you write it.

ⓧ I see raw text instead of a diagram

The extension may not have loaded properly. Try these fixes:

  • Close and reopen the Markdown preview panel.
  • Reload VS Code completely (Cmd+Shift+P or Ctrl+Shift+P, then type Reload Window and press Enter).
  • Check that the extension is listed as Enabled in the Extensions panel.
  • Make sure the code fence starts with exactly ```mermaid (no space between the backticks and the word mermaid).

help me get Mermaid diagrams rendering in VS Code's Markdown preview

✔️ Awesome, I've got everything!

Great. Double check you've saved DESIGN.md and your preview shows the colored diagram.

ⓧ I'd like to double check the full code

Your DESIGN.md should contain two parts, in this order:

  • The starter template with 7 numbered section headings (# StrideSync System Design at the top, followed by sections 1 through 7 each with (to be filled) placeholders).
  • The test Mermaid diagram at the bottom, starting with ```mermaid and ending with ```.

Scroll through your file and confirm both parts are present. The preview pane should show the section headings as formatted text and the Mermaid block as a visual flowchart.

Your environment is ready. Next up, you'll fill in the requirements and run back-of-envelope capacity math that will drive every architecture decision for the rest of the project.

Define Requirements and Estimate Capacity

Your environment is ready and Mermaid diagrams render cleanly in the preview. Now it's time to fill the document with substance.

Every system design interview starts with scoping. Without capacity math, architecture decisions are opinions. With numbers, they become engineering constraints that dictate technology choices. The calculations you write here will directly force design decisions in later steps.

In this step, get ready to:
  • Define functional and non-functional requirements with quantified targets.
  • Calculate back-of-envelope capacity estimates for writes, reads, storage, and fan-out.
  • Derive architecture constraints from the numbers.
Define functional and non-functional requirements

Requirements scoping is the first thing you do in any design interview. It draws a boundary around what the system does and how well it must perform.

  • In DESIGN.md, find the ## 1. Requirements section.
  • Replace everything between ## 1. Requirements and ## 2. Capacity Estimation with the following content:
### Functional Requirements
- Users upload GPS activities (runs, rides, swims) with telemetry data (coordinates, heart rate, elevation, pace)
- Users follow other athletes and see their activities in a chronological feed
- Users can like (give "kudos") and comment on activities
- System sends notifications for new followers, kudos, and comments
- Users view personal analytics (weekly distance, pace trends, elevation gains)

### Non-Functional Requirements
| Metric | Target |
|--------|--------|
| Registered users | 120M |
| Monthly active users (MAU) | 40M |
| Daily active users (DAU) | 10M |
| Activity uploads/day | ~7M |
| Feed read latency | < 200ms p99 |
| Upload acknowledgment latency | < 500ms |
| Availability | 99.9% (three nines) |
| Feed consistency | Eventual (1-3 sec acceptable) |
| Upload consistency | Strong (read-your-writes) |

What do these requirements tell us?

The functional requirements define WHAT the system does. Five features is a reasonable scope for a 45-minute interview. More than that, and you can't go deep enough on any one.

The non-functional requirements define HOW WELL it performs. Notice the two different consistency models. Feeds can lag by 1-3 seconds (eventual), but when you upload an activity, you must see it immediately (strong). This distinction drives the entire async architecture you will build later.

  • Save DESIGN.md.
  • Check the Markdown preview. You should see a clean table with 9 rows of non-functional requirements, each with a concrete numeric target.

Table not rendering properly?

Make sure the separator row (|--------|--------|) has at least three dashes per column. Without it, Markdown treats the lines as plain text instead of a table.

Also confirm there is a blank line between the bullet list and the table header. Markdown needs that whitespace to distinguish block types.

Why isn't my Markdown table rendering in the VS Code preview?

Calculate write path and read path throughput

Now you will derive the system's throughput from first principles. Interviewers want to see the math, not memorized numbers. The formula is simple: QPS = (DAU x actions per user per day) / 86,400 seconds.

  • Find the ## 2. Capacity Estimation section.
  • Replace (to be filled) with the write path and read path tables:
### Write Path (Activity Uploads)
| Metric | Calculation | Result |
|--------|------------|--------|
| Uploads/day | 10M DAU x 5 activities/week / 7 | ~7M/day |
| Average QPS | 7,000,000 / 86,400 | ~81 QPS |
| Peak QPS (3x) | 81 x 3 | ~243 QPS |
| GPS points/activity | 1Hz x 2.5 hours avg | ~10,000 points |
| Peak GPS writes/sec | 243 x 10,000 | ~2.4M points/sec |

### Read Path (Feed)
| Metric | Calculation | Result |
|--------|------------|--------|
| Feed loads/day | 10M DAU x 10 loads/day | 100M/day |
| Average QPS | 100,000,000 / 86,400 | ~11,574 QPS |
| Peak QPS (3x) | 11,574 x 3 | ~34,700 QPS |
| Read:Write ratio | 34,700 / 243 | ~143:1 |

What do these numbers reveal?

The peak multiplier of 3x accounts for traffic spikes (Sunday evening uploads after weekend activities, morning commute feed checks). Consumer apps typically see 2-5x peaks over average.

The 143:1 read-to-write ratio is the headline number. It means this system is overwhelmingly read-heavy. Everyone scrolls the feed constantly, but only a fraction upload an activity on any given day. That ratio will determine your caching strategy.

  • Save DESIGN.md and check the preview. You should see two distinct tables: one for writes (5 rows) and one for reads (4 rows), each showing the calculation alongside the result.

Tables merged into one?

Make sure there is a blank line between the last row of the Write Path table and the ### Read Path (Feed) heading. Without that blank line, Markdown continues the first table instead of starting a new section.

My two Markdown tables are rendering as one single table instead of separate sections.

Calculate storage and fan-out requirements

Throughput tells you how fast. Storage tells you how much. Fan-out tells you how wide each write spreads across the system.

  • Directly below the Read Path table you just added, paste the storage and fan-out tables:
### Storage
| Data Type | Calculation | Daily | Annual |
|-----------|------------|-------|--------|
| GPS points | 7M x 10K points x 48 bytes | ~3.36 TB | ~1.2 PB |
| Raw GPS files | 7M x 480 KB | ~3.36 TB | ~1.2 PB |
| Activity metadata | 7M x 1 KB | ~7 GB | ~2.5 TB |
| User data | negligible | - | ~50 GB |

### Fan-out
| Metric | Calculation | Result |
|--------|------------|--------|
| Avg followers/user | - | 150 |
| Fan-out writes/day | 7M x 150 | ~1.05B/day |
| Average fan-out QPS | 1.05B / 86,400 | ~12,150 QPS |
| Peak fan-out QPS (3x) | 12,150 x 3 | ~36,450 QPS |

Why 48 bytes per GPS point?

Each GPS point records latitude (8 bytes), longitude (8 bytes), elevation (4 bytes), heart rate (2 bytes), speed (4 bytes), cadence (2 bytes), and a timestamp (8 bytes), plus overhead. At 1Hz recording over a 2.5-hour average activity, that yields roughly 10,000 points per upload.

The fan-out number is striking. Each activity upload triggers 150 writes (one per follower) to the feed cache. That single upload becomes 150 downstream operations. At 7M uploads/day, that means over 1 billion fan-out writes daily.

  • Save DESIGN.md and check the preview. You should see four tables now: Write Path, Read Path, Storage (with daily AND annual columns), and Fan-out.

Storage table columns misaligned?

The Storage table has 4 columns while the other tables have 3. Check that the header row and separator row both have exactly 4 pipe-separated columns (| Data Type | Calculation | Daily | Annual |). A missing column in the separator row breaks the entire table.

My Markdown table columns are misaligned or not rendering correctly.

Derive architecture constraints from the numbers

Raw numbers become useful when you translate them into engineering decisions. Each capacity figure eliminates certain technology choices and mandates others.

Before you write this section, look at your capacity tables. What do you think ~2.4M GPS points/sec at peak means for your database choice? Could a single PostgreSQL instance handle that load?

  • Below the Fan-out table, add the constraints summary:
### Constraints Summary
| Metric | Value | Architecture Implication |
|--------|-------|-------------------------|
| Peak feed read QPS | ~35K | Redis cache mandatory |
| Peak GPS points/sec | ~2.4M | Time-series DB required, not OLTP |
| Daily GPS storage | ~3.36 TB | Object storage + time-series DB |
| Fan-out writes/sec | ~36K peak | Async via Kafka |
| Read:Write ratio | ~143:1 | Separate read/write scaling |

Why does this table matter in an interview?

This is where you prove you are not choosing technology by preference. Each row connects a concrete number to a forced architecture decision. A single PostgreSQL node handles 10K-50K queries/second. Your GPS ingest needs 2.4M points/second. That is a 48x gap that no amount of indexing or tuning can close.

Interviewers love this table because it shows you can derive architecture from math, not from blog posts.

  • Save DESIGN.md.

Now add the section divider. Place a line containing only --- on the blank line between the end of your Constraints Summary table and the ## 3. High-Level Architecture heading. Also add one between section 1 and section 2 (after the Non-Functional Requirements table, before ## 2. Capacity Estimation).

  • Save DESIGN.md and check the preview. You should see a horizontal line separating sections 1 and 2, and another separating section 2 from the remaining placeholder sections.

Look at your completed Constraints Summary table. What does the ~143:1 read-to-write ratio tell you about how this system should be optimized?

It confirms the system is overwhelmingly read-heavy. Caching the read path (Redis) and processing writes asynchronously (Kafka) are not optional. They are mathematically required. The numbers also prove that GPS ingestion at 2.4M points/sec is impossible for a standard OLTP database. These two constraints will collide in Step 4 when you design the data layer.

✔️ Awesome, I've got everything!

Double check you have saved DESIGN.md and both sections render correctly in the preview.

ⓧ I'd like to double check the full code

Scroll through your preview and confirm these sections are present in order:

  • Section 1 heading with Functional Requirements (5 bullet points) and Non-Functional Requirements (9-row table).
  • A horizontal divider (---) separating section 1 from section 2.
  • Section 2 with Write Path (5 rows), Read Path (4 rows), Storage (4 rows), Fan-out (4 rows), and Constraints Summary (5 rows).
  • A horizontal divider separating section 2 from the remaining placeholder sections (3 through 7).
  • Sections 3-7 still showing (to be filled) as placeholders.

You now have a quantified foundation for every decision ahead. Next up, you will decompose the system into microservices and draw the high-level architecture diagram that connects them.

Design the High-Level Architecture and API

You have the numbers. Your capacity estimation proves this system needs 35K peak feed reads per second and 2.4M GPS points written per second. Those numbers are constraints, not just statistics.

Constraints without structure are just a table of scary numbers. In this step, you will decompose the system into services with clear boundaries, draw the data flows between them, and define the API contracts that become the interface between those services.

In this step, get ready to:
  • Draw a high-level architecture diagram showing microservice boundaries and data flows.
  • Document what each service owns and what it does not own.
  • Define RESTful API contracts for core endpoints.
Draw the high-level architecture diagram

A single monolith handling uploads, feeds, notifications, and analytics would be impossible to scale independently. Decomposing into microservices lets you scale the feed read path (143:1 read-heavy) separately from the GPS write path.

  • In DESIGN.md, find the line that says (to be filled) under ## 3. High-Level Architecture.
  • Delete (to be filled) and start a Mermaid code fence by typing three backticks followed by mermaid on a new line.
  • Paste the following node definitions inside the code fence to define your system's client-facing layer:
graph TB
    subgraph Clients
        MC[Mobile/Web Client]
    end

    subgraph Gateway Layer
        LB[Load Balancer]
        AG[API Gateway<br/>Rate Limiting, Auth]
    end

    subgraph Application Services
        US[User Service]
        AS[Activity Service]
        FS[Feed Service]
        SS[Social Service]
        NS[Notification Service]
        AN[Analytics Service]
    end

What are these subgraphs?

Each subgraph groups related components. Clients represent mobile and web apps. The Gateway Layer handles authentication and rate limiting before requests reach services. Application Services are the six microservices that own distinct business logic.

  • Directly below the last end (still inside the same mermaid code fence), paste the backend infrastructure layer:
    subgraph Async Processing
        KF[Apache Kafka]
        FW[Fan-out Workers]
        PW[Processing Workers]
    end

    subgraph Data Stores
        PG[(PostgreSQL<br/>Users, Social Graph)]
        TS[(TimescaleDB<br/>GPS Time-Series)]
        RD[(Redis Cluster<br/>Feed Cache, Counters)]
        S3[(S3 Object Storage<br/>Raw GPS Files)]
        CS[(Cassandra<br/>Notifications)]
    end

Why five different data stores?

This is polyglot persistence. Your capacity math showed that no single database can handle both 2.4M GPS writes/sec AND 35K feed reads/sec. Each store is optimized for a specific access pattern: PostgreSQL for relational queries, TimescaleDB for time-series, Redis for sub-millisecond reads, S3 for bulk storage, and Cassandra for high-throughput appends.

  • Save DESIGN.md and open the Markdown preview (Cmd+Shift+V).
  • You should see five labeled subgroups rendered as boxes. No arrows connect them yet.
  • Return to your editor. Below the last end (still inside the mermaid fence), paste the data flow connections and styling:
    MC --> LB --> AG
    AG --> US & AS & FS & SS

    AS -->|raw file| S3
    AS -->|activity.created event| KF
    KF --> FW & PW & NS & AN

    FW -->|write to follower feeds| RD
    PW -->|parsed GPS points| TS

    FS -->|read feed| RD
    SS -->|follow/unfollow| PG
    US -->|profile data| PG
    AN -->|aggregated stats| TS

    style KF fill:#fff3e0
    style RD fill:#e8f5e9
    style PG fill:#e3f2fd
    style TS fill:#fce4ec
  • Close the mermaid code fence by typing three backticks on a new line after the last style line.

What do these arrows represent?

  • MC --> LB --> AG: every client request passes through the load balancer and API gateway before reaching a service.
  • AS -->|activity.created event| KF: the Activity Service publishes an event to Kafka instead of calling downstream services directly. This decouples the upload path from feed generation.
  • KF --> FW & PW & NS & AN: Kafka fans the event out to multiple consumer groups in parallel. Each processes the event independently.
  • The style lines add color to highlight the key infrastructure components.
  • Save DESIGN.md and check the Markdown preview.
  • You should now see a fully connected architecture diagram with arrows showing data flowing from the Mobile Client through the gateway, into services, through Kafka, and into five distinct data stores.

Diagram not rendering?

  • Check that you have exactly one opening ```mermaid line and one closing ``` line wrapping all the content.
  • Make sure there are no extra spaces before the opening backticks.
  • Confirm the Markdown Preview Mermaid Support extension is installed and enabled.

Why isn't my Mermaid diagram rendering in VS Code preview?

Document service responsibilities

A diagram shows structure, but interviewers will ask ownership questions. Each service needs a clear boundary: what it owns, and critically, what it does NOT own.

  • Below the closing ``` of your Mermaid diagram (still inside section 3), add the service responsibilities table by pasting this markdown:
### Service Responsibilities

| Service | Owns | Does NOT Own |
|---------|------|-------------|
| Activity Service | Upload validation, S3 storage, publishing activity.created event | GPS parsing, feed updates, notifications |
| Feed Service | Serving pre-computed feeds from Redis, merging popular athlete posts | Computing or pushing feed entries |
| User Service | Profile CRUD, authentication, authorization | Social graph relationships |
| Social Service | Follow/unfollow, follower/following lists, blocking | Feed generation, notifications |
| Notification Service | Push notifications, in-app notification list, batching/dedup | Deciding business logic for when to notify |
| Analytics Service | Aggregating weekly/monthly stats, trend computation | Real-time activity data |

Why document what a service does NOT own?

Clear negative boundaries prevent feature creep and duplicated logic. If the Activity Service handled feed updates directly, it would couple the upload latency to fan-out performance. By stating "does NOT own feed updates," you force that responsibility into an async consumer.

In interviews, naming what a service excludes demonstrates you have thought about failure domains and deployment independence.

  • Save DESIGN.md and check the preview.
  • You should see a formatted table with six rows, each service having clear "Owns" and "Does NOT Own" columns.
Define core API contracts

API contracts define how services talk to each other and how clients interact with the platform. These become the "interface" you discuss fluently in a design interview.

  • In DESIGN.md, find the line that says (to be filled) under ## 4. API Design.
  • Delete (to be filled) and paste the Activity Upload endpoint contract:
### Activity Upload
```
POST /api/v1/activities
Headers: Authorization: Bearer <token>
Body: multipart/form-data {
    file: <GPX/FIT/TCX file>,
    title: string,
    type: enum(run|ride|swim|hike),
    visibility: enum(public|followers|private)
}
Response: 202 Accepted
{
    "activity_id": "uuid",
    "status": "processing"
}
```

Why 202 Accepted instead of 201 Created?

A 202 Accepted response tells the client the upload was received but processing happens asynchronously. The Activity Service stores the raw file in S3 and publishes an event to Kafka. GPS parsing, feed fan-out, and analytics happen later via consumers.

This keeps the upload latency under the 500ms target from your non-functional requirements.

  • Below the Activity Upload section, paste the Get Feed endpoint:
### Get Feed
```
GET /api/v1/feed?cursor=<timestamp>&limit=20
Response: 200
{
    "activities": [
        {
            "activity_id": "uuid",
            "athlete": { "id": "uuid", "name": "string", "avatar_url": "string" },
            "title": "Morning Run",
            "type": "run",
            "distance_m": 5200,
            "duration_sec": 1560,
            "kudos_count": 12,
            "comment_count": 3,
            "started_at": "2026-07-20T07:30:00Z"
        }
    ],
    "next_cursor": "2026-07-20T06:00:00Z",
    "has_more": true
}
```

What is cursor-based pagination?

Instead of page numbers (which break when new items are inserted), cursor pagination uses a timestamp as a bookmark. The client passes the last-seen timestamp, and the server returns the next page from that point. This is essential for feeds where new content appears constantly.

  • Save DESIGN.md and check the preview to confirm both endpoints render as formatted code blocks with their headers.
  • Below the Get Feed section, paste the remaining three endpoint contracts:
### Give Kudos
```
POST /api/v1/activities/{activity_id}/kudos
Response: 201 Created
{ "kudos_count": 13 }
```

### Follow User
```
POST /api/v1/users/{user_id}/follow
Response: 201 Created
{ "following": true, "follower_count": 342 }
```

### Get Analytics
```
GET /api/v1/users/me/analytics?period=week
Response: 200
{
    "period": "2026-07-14/2026-07-20",
    "total_distance_km": 42.5,
    "total_duration_sec": 14400,
    "activity_count": 5,
    "elevation_gain_m": 320,
    "avg_pace_min_per_km": 5.65
}
```

What do these three endpoints cover?

  • Give Kudos: a simple POST that atomically increments a counter. Returns the updated count so the UI can update without a second fetch.
  • Follow User: modifies the social graph in PostgreSQL and returns the new follower count. This triggers downstream events for feed reconstruction.
  • Get Analytics: reads pre-aggregated stats from the Analytics Service. The period parameter lets clients request weekly, monthly, or custom ranges.
  • Save DESIGN.md and preview.
  • You should see five API endpoint sections, each with a formatted request/response block.

Before you move on, take a moment. Look at your Mermaid diagram and trace a single activity upload from the Mobile Client all the way through to the data stores. Can you name every hop and explain why it exists?

Your diagram should show 6 microservices, an API Gateway, Kafka as the event backbone, and 5 data stores. Every arrow represents a data flow you can explain.

Missing a section in your preview?

  • Make sure section 3 has a --- horizontal rule between it and section 4 (matching the original template structure).
  • Confirm each API code fence uses plain ``` (no language specified) so it renders as a generic code block.

Help me check my architecture diagram looks correct and all API sections render properly.

✔️ Awesome, I've got everything!

Great. Double check you have saved DESIGN.md and that your preview shows both the rendered Mermaid diagram and all five API endpoint sections.

ⓧ I'd like to double check the full code

Scroll through your DESIGN.md and confirm these blocks are present in order:

  • Section 3 heading: ## 3. High-Level Architecture followed by one complete mermaid code fence containing 5 subgraphs (Clients, Gateway Layer, Application Services, Async Processing, Data Stores) plus connection lines and 4 style lines.
  • Service Responsibilities: a ### Service Responsibilities heading followed by a 6-row markdown table.
  • A --- horizontal rule separating section 3 from section 4.
  • Section 4 heading: ## 4. API Design followed by 5 subsections (Activity Upload, Get Feed, Give Kudos, Follow User, Get Analytics), each with a ### heading and a code-fenced request/response block.

Your system now has shape. Services have boundaries, data has a path from upload to feed, and the APIs define a clean contract between them. Next up, you will dive into the data layer and prove that your store choices hold up under the write-throughput constraints your numbers demand.

Design the Data Layer

Your architecture diagram already shows five different data stores. That wasn't a random guess. The capacity math from Step 2 dictates exactly why a single database cannot serve every access pattern.

In this step, you'll confront the hard numbers that force polyglot persistence: using the right database for each workload. You'll design schemas that respect each store's strengths and document how the system scales horizontally when one node isn't enough.

In this step, get ready to:
  • Map each access pattern to its optimal data store with quantified justification.
  • Design schemas for PostgreSQL, TimescaleDB, and Redis.
  • Document a sharding strategy for horizontal scaling.
Calculate the single-database bottleneck

Before choosing any databases, ask yourself: what if you just used PostgreSQL for everything?

Look at your Constraints Summary table from Step 2. Your peak GPS ingestion is ~2.4M points/sec. A single PostgreSQL node handles 10K-50K queries/sec. What's the gap?

  • In your document, find the placeholder text under the section 5 heading:
## 5. Data Layer
(to be filled)
  • Replace (to be filled) with the following bottleneck calculation:
## 5. Data Layer

PostgreSQL peak write capacity: ~50K QPS. My GPS ingestion need: ~2.4M points/sec. Gap: 48x. Conclusion: PostgreSQL alone cannot serve as the GPS store.

Why does this gap matter?

This 48x gap is your engineering justification for polyglot persistence. In an interview, this single calculation proves you're choosing TimescaleDB based on constraints, not preference.

The same logic applies to feeds: ~35K peak read QPS exceeds what PostgreSQL delivers comfortably, which justifies a Redis caching layer.

  • Save DESIGN.md and check the Markdown preview.
  • You should see your bottleneck sentence rendered under the Data Layer heading.
Map access patterns to data stores

Now that you've proven a single database won't work, map each data type to the store that fits its access pattern. This table is the centerpiece of your data layer justification.

  • Below your bottleneck sentence in DESIGN.md, add this mapping table:
### Access Pattern to Data Store Mapping

| Data Type | Store | Justification | Access Pattern |
|-----------|-------|---------------|----------------|
| User profiles | PostgreSQL | ACID, relational joins, strong consistency | Read-heavy, low volume |
| Social graph | PostgreSQL | Bidirectional queries, joins | Write on follow, read on feed build |
| Activity metadata | PostgreSQL | Structured, queryable, consistent | Write once, read on view |
| GPS time-series | TimescaleDB | Hypertable partitioning, compression, range queries | Bulk write on ingest, range-read |
| Raw GPS files | S3 | Infinite scale, cheap, write-once | Write once, rare re-reads |
| Feed cache | Redis Sorted Sets | O(1) reads, sorted by timestamp | Write on fan-out, read on feed load |
| Kudos counts | Redis Counters | Atomic INCR, sub-ms reads | High-frequency increment |
| Notifications | Cassandra | High write throughput, time-ordered | Append-heavy, read recent N |

What does this table communicate?

Each row connects a data type to the store that matches its access pattern. The "Justification" column is where interview value lives. It shows you chose each store based on constraints, not just familiarity.

TimescaleDB handles GPS because it extends PostgreSQL with hypertable partitioning, compressing old data automatically. Cassandra handles notifications because its LSM-tree architecture is optimized for high-volume appends.

  • Save DESIGN.md and check your Markdown preview.
  • You should see a formatted table with 8 rows, one per data type, with clean column alignment.

Table not rendering correctly?

Make sure there is a blank line before the table header row. Markdown requires blank lines before block elements like tables.

Check that every row has the same number of pipe characters. A missing pipe will break the entire table formatting.

help me fix my markdown table rendering in VS Code preview

Design the PostgreSQL schemas

PostgreSQL holds three tables: users, the social graph (follows), and activity metadata. These all need ACID guarantees and relational joins.

  • Below the mapping table, add the schemas heading and the users/follows tables:
### Schemas

#### PostgreSQL
```sql
CREATE TABLE users (
    user_id UUID PRIMARY KEY,
    username VARCHAR(30) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    display_name VARCHAR(100),
    avatar_url TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE follows (
    follower_id UUID REFERENCES users(user_id),
    following_id UUID REFERENCES users(user_id),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (follower_id, following_id)
);
CREATE INDEX idx_follows_following ON follows(following_id);
```

What do these schemas do?

The follows table uses a composite primary key (follower_id, following_id) which prevents duplicate follows and makes "who do I follow?" queries fast.

The idx_follows_following index enables the reverse query: "who follows me?" Without it, that query would require a full table scan.

  • Now add the activities table directly below, still inside the PostgreSQL section:
```sql
CREATE TABLE activities (
    activity_id UUID PRIMARY KEY,
    user_id UUID REFERENCES users(user_id),
    title VARCHAR(255),
    activity_type VARCHAR(20) NOT NULL,
    distance_m FLOAT,
    duration_sec INTEGER,
    elevation_gain_m FLOAT,
    avg_speed_mps FLOAT,
    started_at TIMESTAMPTZ NOT NULL,
    visibility VARCHAR(20) DEFAULT 'public',
    kudos_count INTEGER DEFAULT 0,
    comment_count INTEGER DEFAULT 0,
    s3_raw_file_key TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_activities_user_time ON activities(user_id, started_at DESC);
CREATE INDEX idx_activities_created ON activities(created_at DESC);
```

Why two indexes on activities?

The idx_activities_user_time index serves "show me my activities in reverse chronological order." The composite key (user_id + started_at DESC) makes this a single index scan.

The idx_activities_created index serves the global "recent activities" query used when rebuilding feed caches on a miss.

  • Save DESIGN.md and preview.
  • You should see two SQL code blocks with syntax highlighting. The first has the users and follows tables. The second has the activities table with two CREATE INDEX statements.
Design the TimescaleDB and Redis schemas

Your GPS ingestion layer needs a schema that handles 2.4M points/sec at peak and compresses old data automatically. Redis needs structure for the feed cache and kudos counters.

  • Add the TimescaleDB schema below your PostgreSQL section:
#### TimescaleDB (GPS Points)
```sql
CREATE TABLE gps_points (
    activity_id UUID NOT NULL,
    recorded_at TIMESTAMPTZ NOT NULL,
    latitude DOUBLE PRECISION NOT NULL,
    longitude DOUBLE PRECISION NOT NULL,
    elevation_m FLOAT,
    heart_rate SMALLINT,
    speed_mps FLOAT,
    cadence SMALLINT
);

SELECT create_hypertable('gps_points', 'recorded_at',
    chunk_time_interval => INTERVAL '1 day');

CREATE INDEX idx_gps_activity ON gps_points(activity_id, recorded_at);

ALTER TABLE gps_points SET (timescaledb.compress,
    timescaledb.compress_segmentby = 'activity_id');
SELECT add_compression_policy('gps_points', INTERVAL '7 days');
```

What does the hypertable do?

The create_hypertable call converts gps_points into a time-partitioned table. Each day's data lives in its own chunk, so queries for a specific activity only scan the relevant chunk instead of the entire table.

The compression policy automatically shrinks data older than 7 days. This is how you handle 3.36 TB/day without storage costs exploding.

  • Now add the Redis data structures below the TimescaleDB section:
#### Redis Data Structures
```
# Per-user feed (sorted set, score = unix timestamp)
ZADD feed:{user_id} {timestamp} {activity_id}
ZREMRANGEBYRANK feed:{user_id} 0 -501   # trim to 500 entries

# Popular athlete posts (separate sorted set)
ZADD popular:{athlete_id} {timestamp} {activity_id}
ZREMRANGEBYRANK popular:{athlete_id} 0 -101  # keep last 100

# Kudos counter
INCR kudos:{activity_id}

# Read feed
ZREVRANGE feed:{user_id} 0 19 WITHSCORES
```

Why sorted sets for feeds?

Redis sorted sets store items ranked by a score. Using the activity timestamp as the score means ZREVRANGE returns the most recent activities in O(log N + M) time, where M is the number returned. For a 20-item feed page, this is sub-millisecond.

The ZREMRANGEBYRANK trim keeps each feed capped at 500 entries. Older items are evicted automatically, bounding Redis memory usage per user.

  • Save DESIGN.md and check the preview.
  • You should see three sub-sections under Schemas: PostgreSQL (two SQL blocks), TimescaleDB (one SQL block), and Redis Data Structures (one code block showing ZADD/ZREVRANGE commands).

SQL syntax highlighting not appearing?

Make sure each SQL block starts with exactly three backticks followed by sql (no space before the language tag). The closing line must be exactly three backticks with nothing else on the line.

help me debug markdown code block syntax highlighting

Document the sharding strategy

A single node of each database won't scale forever. Your sharding plan explains how each store grows horizontally when capacity is exhausted.

  • Below the Redis section, add the sharding strategy:
### Sharding Strategy
- **PostgreSQL**: Shard by user_id at ~50M users. Follows table denormalized into `following` (partition by follower_id) and `followers` (partition by following_id) to avoid cross-shard queries.
- **TimescaleDB**: Automatic time-based chunking (1 chunk per day). Compressed after 7 days. Multi-node distributes by activity_id hash for parallel ingest.
- **Redis Cluster**: Hash-slot sharding by user_id. 6+ nodes with replicas. Hot keys (popular athletes) served from read replicas.

Why denormalize the follows table when sharding?

The follows table needs two queries: "who do I follow?" (keyed by follower_id) and "who follows me?" (keyed by following_id). If you shard by follower_id only, the second query becomes a scatter-gather across all shards.

Denormalizing into two tables (one partitioned each way) trades storage for query locality. Each query hits a single shard.

  • Save DESIGN.md and preview.
  • You should see a bulleted list with three bold database names, each followed by their sharding approach.

Now go back and delete the bottleneck sentence you wrote at the top of section 5. The mapping table and schemas communicate the same reasoning more formally. Your section should start with the access pattern table heading.

  • Find and delete this line from the top of section 5:
PostgreSQL peak write capacity: ~50K QPS. My GPS ingestion need: ~2.4M points/sec. Gap: 48x. Conclusion: PostgreSQL alone cannot serve as the GPS store.
  • Save DESIGN.md.
  • Your section 5 should now start with the "Access Pattern to Data Store Mapping" heading directly after the section title.

Before you move on, trace the bottleneck logic end-to-end. Can you point to the exact row in your mapping table that resolves the 48x PostgreSQL gap you calculated earlier?

The GPS time-series row should read: TimescaleDB, justified by hypertable partitioning and compression. That row is the direct engineering answer to the bottleneck calculation. It proves your polyglot design is constraint-driven.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and that section 5 renders cleanly in the Markdown preview.

ⓧ I'd like to double check the full code

Scroll through your section 5 in the preview and confirm these chunks appear in order:

  • Access Pattern to Data Store Mapping table (8 data-type rows).
  • Schemas heading, then PostgreSQL sub-heading with two SQL code blocks (users/follows and activities).
  • TimescaleDB sub-heading with one SQL code block (gps_points with hypertable and compression).
  • Redis Data Structures sub-heading with one code block showing ZADD, ZREMRANGEBYRANK, INCR, and ZREVRANGE commands.
  • Sharding Strategy heading with three bullet points (PostgreSQL, TimescaleDB, Redis Cluster).

Your data layer now has five stores, each justified by the access patterns and throughput constraints from your capacity math. Next up, you'll design the feed system and async pipeline that connects these stores together.

Design the Feed System, Async Pipeline, and Bottleneck Analysis

Your data layer is in place, with each store justified by the access patterns you mapped in the previous step. Now you face the hardest problem in the system: delivering a real-time feed to 10M daily users.

The feed is the core product surface. Your capacity math already shows 36K peak fan-out writes/sec and 35K peak feed reads/sec. A naive approach of pushing every activity to every follower's feed sounds simple, but the math reveals a fatal flaw for popular athletes. This step designs the hybrid strategy that solves it, wires the async pipeline through Kafka, and identifies exactly where the system breaks under load.

In this step, get ready to:
  • Design a hybrid fan-out strategy that handles both normal and popular athletes.
  • Build a Kafka event flow diagram showing the full async pipeline from upload to feed delivery.
  • Identify bottlenecks, document consistency trade-offs, and define monitoring thresholds.
Design the hybrid fan-out strategy

Consider what happens with a naive "fan-out on write" approach where every activity upload pushes to every follower's feed. What breaks when a popular athlete with 500K followers uploads an activity?

Redis handles ~100K operations/sec per instance. A single upload from that athlete triggers 500K Redis writes. That takes ~5 seconds, blocking the entire async pipeline for one upload. This is the constraint that forces a hybrid approach.

  • In DESIGN.md, find the section 6 placeholder:
## 6. Feed System & Async Pipeline
(to be filled)
  • Replace it with the hybrid fan-out strategy by pasting this content:
## 6. Feed System & Async Pipeline

### Hybrid Fan-Out Strategy

**Threshold: 10,000 followers**

| Athlete Type | Strategy | Feed Write Cost | Feed Read Cost |
|-------------|----------|-----------------|----------------|
| Normal (< 10K followers) | Fan-out on Write (Push) | O(followers) Redis writes | O(1) ZREVRANGE |
| Popular (>= 10K followers) | Fan-out on Read (Pull) | O(1) single store write | O(1) + O(C) merge (C = popular athletes followed) |

**Why 10K?**
- Below 10K: fan-out = 10K writes at 100K ops/sec = 100ms. Acceptable.
- Above 10K: fan-out = 500K writes = 5 seconds blocking the pipeline. Unacceptable.
- Tunable based on observed Kafka consumer lag in production.

What does this strategy do?

The threshold of 10,000 followers splits athletes into two groups with different feed delivery strategies.

  • Normal athletes (< 10K): their activity is pushed to every follower's Redis feed at upload time. Feed reads become a single O(1) lookup.
  • Popular athletes (>= 10K): their activity is stored once in a dedicated sorted set. At read time, the Feed Service merges it with the pre-computed feed. This trades slightly slower reads for massively reduced write amplification.
  • Save DESIGN.md and check the Markdown preview.
  • You should see the strategy table rendered with two rows (Normal and Popular) and the "Why 10K?" justification below it.

Now add the event flow diagram that shows how an activity upload travels through Kafka to reach follower feeds.

  • Below the fan-out strategy section, add a ### Event Flow heading followed by this Mermaid diagram:
### Event Flow

```mermaid
graph TB
    AS[Activity Service] -->|publish| KT[Kafka: activity.created]

    KT --> FR{Fan-out Router<br/>Check follower count}
    KT --> PW[GPS Processing Workers]
    KT --> NW[Notification Workers]
    KT --> AW[Analytics Workers]

    FR -->|< 10K followers| FW[Fan-out Workers]
    FR -->|>= 10K followers| PS[Popular Athlete Store<br/>Redis sorted set]

    FW -->|ZADD per follower| RC[(Redis Feed Cache)]

    subgraph Feed Read Path
        FS[Feed Service]
        FS -->|1. ZREVRANGE feed:user_id| RC
        FS -->|2. Query popular athletes user follows| PS
        FS -->|3. Merge and rank by timestamp| MR[Merged Result]
        MR -->|4. Hydrate metadata| PG[(PostgreSQL)]
    end

    PW -->|bulk INSERT| TS[(TimescaleDB)]
    NW -->|batch + deliver| PUSH[Push Notifications]
    AW -->|aggregate| STATS[(Analytics Aggregates)]

    style KT fill:#fff3e0
    style RC fill:#e8f5e9
    style FR fill:#ffebee
```

What does this diagram show?

This diagram traces the full lifecycle of an activity upload through the async pipeline.

  • The Fan-out Router checks the athlete's follower count and routes to either the push path (Fan-out Workers writing to each follower's Redis feed) or the pull path (Popular Athlete Store).
  • The Feed Read Path merges both sources at query time: the user's pre-computed feed plus recent posts from popular athletes they follow, hydrated with metadata from PostgreSQL.
  • GPS Processing Workers, Notification Workers, and Analytics Workers consume the same Kafka event independently, decoupling all downstream processing from the upload acknowledgment.
  • Save DESIGN.md and check the Markdown preview.
  • You should see a rendered flowchart with an orange Kafka node, a red Fan-out Router diamond branching into two paths, a green Redis Feed Cache, and a "Feed Read Path" subgraph showing the 4-step merge process.

Diagram not rendering?

  • Make sure the opening fence says exactly ```mermaid with no spaces before or after the word "mermaid".
  • Confirm you have the Markdown Preview Mermaid Support extension installed and active. Check the Extensions panel for a blue checkmark.
  • If you see raw text instead of a diagram, close and reopen the preview panel (Cmd+Shift+V).

Why isn't my Mermaid diagram rendering in VS Code preview?

Design the notification pipeline

Notifications are a separate concern from the feed. They need batching (so a user doesn't get 50 individual kudos pushes) and rate limiting to prevent notification fatigue.

  • Below the Event Flow Mermaid diagram, add the notification pipeline table by pasting this content:
### Notification Pipeline
| Event | Kafka Topic | Delivery | Batching |
|-------|-------------|----------|----------|
| New follower | social.follow.created | Push + in-app | None (immediate) |
| Kudos received | social.kudos.created | Push + in-app | 5-second window ("Alice and 3 others") |
| Comment received | social.comment.created | Push + in-app | None (immediate) |
| Followed athlete posts | activity.created | In-app badge only | Feed-level (not individual push) |

Rate limit: max 10 push notifications per user per hour.

Why batch kudos but not comments?

Kudos arrive in bursts (many people kudos a popular activity within minutes). Sending individual pushes for each one is noisy. A 5-second batching window collapses "Alice, Bob, Carol, Dave gave kudos" into a single notification.

Comments are rarer and carry unique content the user wants to read immediately, so they go out unbatched. Followed-athlete posts never trigger push notifications. They only update the feed badge to avoid overwhelming active followers.

  • Save DESIGN.md and check the Markdown preview.
  • You should see a clean four-row table with Kafka topic names, delivery methods, and batching strategies for each notification type.
Identify bottlenecks and define monitoring

This is the highest-signal section in a design interview. Interviewers want to see that you can identify where your own design breaks and articulate specific mitigations, not generic "add more servers" answers.

  • Find the section 7 placeholder in DESIGN.md:
## 7. Bottleneck Analysis & Monitoring
(to be filled)
  • Replace it with the bottleneck analysis by pasting this content:
## 7. Bottleneck Analysis & Monitoring

### Identified Bottlenecks

| Bottleneck | Breaking Point | Mitigation |
|-----------|----------------|------------|
| Redis hot keys | Popular athlete feed read by 100K+ concurrent users | Read replicas; app-server local cache (1s TTL) |
| Kafka consumer lag | Post-marathon burst (10x normal uploads in 1 hour) | Auto-scale consumer group; geographic partitioning |
| TimescaleDB ingest | GPS bulk writes exceed node capacity | Multi-node distribution; batch 1000 points/INSERT |
| Fan-out storm | 100 athletes finish race simultaneously | Backpressure; priority queues in Kafka |
| PostgreSQL follows | Celebrity with 5M followers queried | Paginated queries; cached follower counts |

What makes a good bottleneck analysis?

Each row names a specific breaking scenario (not vague "high traffic"), quantifies when it breaks (100K concurrent, 10x burst), and proposes a concrete mitigation. This demonstrates that you understand the failure modes of your own design, which is exactly what senior interviews test for.

  • Save DESIGN.md and confirm the bottleneck table renders in preview.

Now add the consistency decisions and monitoring metrics that complete the analysis.

  • Below the bottleneck table, add the consistency and monitoring sections by pasting this content:
### Consistency Decisions

| Data | Model | Justification |
|------|-------|---------------|
| Activity upload | Strong (read-your-writes) | User must see own activity immediately |
| Feed entries | Eventual (1-3 sec) | Imperceptible delay; enables async fan-out |
| Kudos count | Eventual (batched) | Display metric; +/- 5 acceptable |
| Follower count | Eventual | Cached; async update from graph |
| Analytics | Eventual (hourly) | Dashboard; freshness not critical |

### Key Monitoring Metrics

| Metric | Target | Alert Threshold |
|--------|--------|----------------|
| Feed latency p99 | < 200ms | > 500ms |
| Kafka consumer lag | < 1,000 msgs | > 10,000 msgs |
| Redis cache hit rate | > 95% | < 90% |
| Upload-to-feed time | < 30s | > 60s |
| API error rate | < 0.1% | > 1% |
| GPS ingest throughput | > 2M points/sec | < 1.5M points/sec |

Why document consistency decisions explicitly?

Every distributed system makes consistency trade-offs. Documenting them shows you made deliberate choices, not accidental ones. Strong consistency for uploads means the user sees their own activity immediately. Eventual consistency for feeds enables the entire async fan-out architecture that makes the system scalable.

  • Save DESIGN.md and check the Markdown preview.
  • You should see three tables in Section 7: Identified Bottlenecks (5 rows), Consistency Decisions (5 rows), and Key Monitoring Metrics (6 rows with targets and alert thresholds).

Tables not rendering correctly?

  • Markdown tables require the header separator row (the row of dashes and pipes). Make sure each table has a |---|---|---| line after the header.
  • Check that each row has the same number of pipe characters. A missing pipe in one cell breaks the entire table rendering.

Why isn't my Markdown table rendering properly in VS Code preview?

Your design document is now complete. Before you finish, trace a single activity upload through the entire system to verify your architecture holds together.

Starting from the Event Flow diagram, can you follow one upload from publish to feed delivery and name every component it touches?

  • Open the Markdown preview and scroll to the Event Flow diagram in Section 6.
  • Trace the path: Activity Service publishes to the Kafka activity.created topic. The Fan-out Router checks follower count. For a normal athlete (< 10K followers), Fan-out Workers execute ZADD per follower into Redis. For a popular athlete (>= 10K followers), the activity goes to the Popular Athlete Store.
  • Now trace the Feed Read Path: Feed Service reads the user's pre-computed feed via ZREVRANGE, queries popular athletes they follow, merges by timestamp, and hydrates metadata from PostgreSQL.

Why this trace matters

In an interview, you'll be asked to "walk me through what happens when a user uploads an activity." Being able to trace the full path, naming every component, data store, and async boundary, demonstrates that your architecture is coherent rather than a collection of disconnected diagrams.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and that all seven sections are filled in (no remaining "(to be filled)" placeholders).

ⓧ I'd like to double check the full code

Your DESIGN.md is now too long to display in one block. Scroll through your file and confirm these sections are present, in order:

  • Section 1: Requirements (functional + non-functional table).
  • Section 2: Capacity Estimation (write path, read path, storage, fan-out, constraints summary tables).
  • Section 3: High-Level Architecture (rendered Mermaid diagram + service responsibilities table).
  • Section 4: API Design (5 endpoint contracts).
  • Section 5: Data Layer (access pattern mapping, PostgreSQL/TimescaleDB/Redis schemas, sharding strategy).
  • Section 6: Feed System & Async Pipeline (hybrid fan-out table, event flow Mermaid diagram, notification pipeline table).
  • Section 7: Bottleneck Analysis & Monitoring (bottleneck table, consistency decisions table, monitoring metrics table).

Secret mission

Redesign for 100M Daily Active Users

Scaling 10x from 10M to 100M DAU does not mean "add more servers." It fundamentally changes which components become bottlenecks and forces architectural rethinking around geographic distribution, data partitioning, and hot-key economics. This is the follow-up question that separates strong candidates from exceptional ones in system design interviews.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The stridesync-design directory on your Desktop containing DESIGN.md and DESIGN-100M-DAU.md.
  • The Markdown Preview Mermaid Support extension in VS Code.

Keep everything running

No action needed. Choose this if you plan to refine your design document further, add more diagrams, or use it as reference material for upcoming interviews.

  • Your stridesync-design folder stays on your Desktop with both Markdown files intact.
  • The Mermaid preview extension remains installed in VS Code for future Markdown diagramming.
  • Consider pushing the folder to a GitHub repository as a portfolio piece.

Pause - I'll come back to this later

Since everything is local files and a VS Code extension, there is nothing to pause. Your files remain on disk and the extension stays installed until you choose to remove them.

  • No processes are running that need stopping.
  • Your files will be exactly where you left them when you return.

Delete - I don't want to use this again

Remove all project files and the VS Code extension to start fresh.

  • Delete the project directory by running this command in your terminal:
rm -rf ~/Desktop/stridesync-design
  • On Windows, run this instead:
Remove-Item -Recurse -Force ~\Desktop\stridesync-design
  • Uninstall the Mermaid extension by opening the Extensions panel in VS Code (Cmd+Shift+X on macOS or Ctrl+Shift+X on Windows).
  • Search for Markdown Preview Mermaid Support, click on it, then click Uninstall.

Nice Work!

Nice Work!

You just designed a distributed system that handles 10 million daily active users uploading GPS activities, consuming real-time feeds, and engaging with a social network of athletes.

You've learned how to:

  • Apply the structured system design methodology (requirements, capacity estimation, high-level architecture, deep-dive, bottleneck analysis) to scope a complex distributed system with quantified engineering constraints.
  • Design a polyglot data layer (PostgreSQL + TimescaleDB + Redis + S3 + Cassandra) and a hybrid fan-out feed system justified by capacity math, not guesswork.
  • Identify concrete bottlenecks and articulate consistency trade-offs with explicit reasoning, using Kafka event-driven pipelines to decouple upload acknowledgment from downstream processing.
  • Secret Mission: Extended the design to 100M DAU with multi-region architecture, geographic sharding, and hot-key mitigation strategies.

Ready to quiz yourself?