STUDY GUIDEEDUCATION·ENGLISH EDITION
Video thumbnail 3:16:48
575Kviews
7.9Klikes
216comments
Oct 13, 2026published
Figures as shown on YouTube on Nov 2, 2026.

Design Software Systems Like a Senior Engineer – Complete Course

System Design: design like a senior engineer

A study guide to the freeCodeCamp course: the five-step framework, then Spotify, Booking.com and WhatsApp designed from scratch with the speaker's own numbers and math, each section linked to its minute in the video.

28 video chapters19 guide sections98 flashcards
freeCodeCamp.org
freeCodeCamp.org
@freecodecamp · 12M subscribers
Software engineer who teaches system design on his YouTube channel; author of this freeCodeCamp course
#SystemDesign#SystemDesignInterview#SoftwareArchitecture#Scalability#DistributedSystems
Quick mode is on: you see only each section's key takeaways. Use the same switch to return to the full explanation.

Source: the video's subtitles. Examples and numbers come from the speaker; anything I added is marked (Explainer's note).

Other language editions are one click away in the language switch.

TL;DR · IN A HURRY

The 5-minute summary

The key points of the whole video, in minutes

Short on time? Read these ten points and the decision table below them. Each point links to the section that covers it in depth, and the Quick mode switch reduces every section to its key takeaways.

  1. The interview evaluates you on five things, not one: communication, structured thinking, technical judgment, practical depth and time management. Memorizing ready-made architectures covers only one dimension. Section 0
  2. The framework is five steps in 60 minutes: requirements (5–10 min), then data and API and the high-level design (15–20 min each), then scale and reliability (5–10 min), and finally trade-offs and extensions. Section 1
  3. The interview is a conversation between engineers: ask before you draw, present more than one solution, explain the trade-off behind every choice, and check with the interviewer at the end of each phase. Section 2
  4. The estimates show you where the weight is: in Spotify, audio dominates storage and bandwidth (90 TB versus 3 GB of metadata), so audio goes to blob storage and a CDN and metadata goes to SQL. Section 3
  5. Start simple and scale gradually: stateless API servers, read replicas, then sharding, and a CDN in every region. This is what sets a senior apart: planning first, then technology choices, and code last. Section 6
  6. Booking.com is a read-heavy system: about 1,000 read QPS versus 17 write TPS, so search goes to Elasticsearch, and booking stays on PostgreSQL for ACID. Section 7
  7. Double booking has two causes and two fixes: the same user clicks twice, fixed by a reservation ID generated on the client; and two people book the last room, fixed by SELECT ... FOR UPDATE, or Serializable if the TPS is low. Section 9
  8. Shard by the key your queries filter on: hotel_id for hotels and user_id for messages. Modulo if the number of shards is fixed, consistent hashing if you will add many. Section 10
  9. WhatsApp: WebSocket for real-time connections, Kafka between receiving and storing, Cassandra for massive writes, Redis for each user's location and the offline inbox, and pre-signed URLs and a CDN for media. Section 11
  10. Design for failure once the core design holds: fallback from Redis to the database, a local buffer if Kafka goes down, a dead letter queue, rate limiting, and feature flags for gradual rollout. Section 15

Decision table: in this situation, use what?

Situation What to use Section
Relational data with many joins (playlists, songs, artists) SQL such as PostgreSQL or MySQL 5
Large files (audio, images, video) Blob storage such as S3 with signed URLs and a CDN 4
A booking or payment that must be 100% consistent SQL with ACID transactions 9
Billions of writes per day and simple queries NoSQL such as Cassandra or DynamoDB 11
Full-text search, typos and distance filtering Elasticsearch synced with the DB through CDC 10
Reads far outnumber writes Read replicas; a read right after a write goes to the primary 10
Data larger than one database Sharding by the key you filter on (hotel_id, user_id) 10, 14
You will keep adding shards Consistent hashing instead of modulo 10
The user clicks "Book" twice A client-generated reservation ID as an idempotency key 8
Two people book the last room at the same moment A row lock with SELECT ... FOR UPDATE 9
Data that is read often and rarely changes A Redis cache with a TTL, invalidated after every booking 10
Real-time messages in both directions WebSocket with a Layer 4 load balancer 11
Receiving must stay fast and storing can be delayed A message queue (Kafka), batch writes and a dead letter queue 13
The recipient is offline An inbox in Redis and the DB, and a push notification through APNs or FCM 12
Someone is spamming or scraping Token bucket rate limiting in a central Redis 13
Redis is down Fall back to reading from the database, then sync when it returns 15
A new feature you want to try on part of the users A feature flag with a gradual rollout (for example 20%) 15
VIDEO · CHAPTERS

Official video chapters

All 28 chapters as listed in the video description. Pick one to open the video there.
  1. Why System Design Matters
  2. Interview Formats Overview
  3. The 5 Step Framework
  4. Tradeoffs and Extensions
  5. Requirements and Capacity Planning
  6. Architecture and Data Flows
  7. API Design
  8. Storage and Schema Design
  9. Scaling to Millions
  10. Non-Functional Targets
  11. Capacity Math and Architecture
  12. API Design and Idempotency
  13. Data Modeling and Inventory Logic
  14. Concurrency and Race Conditions
  15. Sharding, Caching and Scaling
  16. Search at Scale (Elasticsearch)
  17. Replication and Global Routing
  18. Scale Estimation
  19. Architecture and Real Time Communication
  20. Data Modeling and API Flows
  21. Multi Device Sync and Offline Delivery
  22. Fanout, Media Storage and CDN Delivery
  23. Event Driven Architecture and Batch Processing
  24. Encryption and Rate Limiting
  25. Sharding and Cleanup Jobs
  26. Failure Handling and Global Scaling
  27. Feature Flags and Rollouts
  28. How to Actually Get Better
OVERVIEW

Why System Design matters, and how it is evaluated

4 min read 00:31

The speaker opens by saying that most software engineering jobs today require System Design knowledge, both in the work itself and in the interview, and that for senior and staff roles it is one of the factors that decide the outcome. He recounts that early in his career he failed many interviews while applying for senior roles, and what they all had in common was that System Design was almost always part of the discussion.

He adds that you can argue that today it is expected even from mid-level engineers, especially now all engineers use AI to write the implementation. So companies want to know whether you understand the big picture: how components talk to each other, how to design a solution from scratch, and how to discuss trade-offs. The depth expected varies by level, but you need to be ready either way.

The course then runs like this: the framework first, then applying it to three real systems: Spotify, Booking.com and WhatsApp.

0.1System Design interview formats 02:11

Format How it works
Whiteboarding (the most common) A session where you design a whole system or a feature from scratch
Bottleneck discussion They show you problems in the company's current infrastructure and see how you would propose solutions
Reviewing your past projects Questions about the architecture of things you built, to make sure you really understand them
Conceptual questions They test how deeply you understand System Design concepts

And even if you know all the concepts, you still need a framework to guide you through a 45- or 60-minute discussion and get the most out of it.

0.2Why is this interview hard? 03:33

In live coding (whether a LeetCode problem or a real-world challenge) there is a clear input and output: you know the argument types, you know what you are supposed to return, and you can run the code or try test cases on the whiteboard. It is clear whether your solution works or not.

System Design is the exact opposite:

  • The requirements are vague, and your job is to clarify them and lock down the scope.
  • There is no single right answer for the solution you propose.
  • The interview tests how you think, not just what you know, or whether you reach "the right solution".

0.3The five dimensions you are evaluated on 05:16

Most candidates prepare by memorizing a few architectures (Spotify, WhatsApp...). That is a good step, but it is one dimension out of five:

Dimension The question the interviewer asks themselves What it means in real work
Communication & collaboration Do they ask clarifying questions? Do they check their assumptions as they go? If your manager handed you a big, unclear feature: would you assume, build, and discover it is not what was wanted, or would you clarify first? And are you easy to work with?
Structured thinking Do they break the vague problem into concrete steps, or jump between random decisions? A strong candidate has an outline: they know where they are now and what comes next
Technical judgment When they choose a database, a cache or a message queue, can they explain why? The choice is based on the requirements, not on habit, and not on "I know Kafka" when another queue fits better
Practical depth Do they think about failure modes, scale and the things that can break? It separates someone who has actually shipped production systems from someone who only learned the theory
Time management Did they avoid getting stuck on one part and spending all the time on the high-level design? If you can't manage your time in the interview, you probably can't manage it on the job

The speaker's time-management tip: ask the interviewer early what they want to focus on, or propose it yourself and get their agreement, and keep an eye on the clock: how much have you spent on this part? If you are running long, move on.

BASICS

The framework: five steps for any interview

5 min read 09:21

This is a structure you repeat in any whiteboarding interview, and its purpose is to keep you organized and show the interviewer how you think throughout the 45 or 60 minutes. The 60-minute format is the most common, because there is a lot to cover.

# Step Time (out of 60 minutes) The question you answer
1 Clarify requirements 5–10 minutes What are we building, who uses it, and what is out of scope?
2 Data & API layer 15–20 minutes What do we store, and how does the client talk to the system?
3 High-level architecture about 15 minutes (15–20) What are the components, and how does data flow through them?
4 Scale, bottlenecks & reliability 5–10 minutes What breaks under load, and how do we handle failures?
5 Trade-offs & extensions The remaining time, briefly What did you sacrifice and why, and what would you improve with more time?
flowchart LR
    A[1. Requirements<br/>5-10 min] --> B[2. Data + API<br/>15-20 min]
    B --> C[3. High-level design<br/>~15 min]
    C --> D[4. Scale + Reliability<br/>5-10 min]
    D --> E[5. Trade-offs + Extensions]
    B -.-|order can swap| C

(Explainer's note): The diagram's layout is mine. The speaker said the order of steps 2 and 3 is flexible, and that he personally usually starts with the high-level design.

1.1Step one: clarify the requirements 11:16

The most common mistake candidates make is starting to design before they understand the scope.

  • Functional requirements: what are the core use cases? And what is explicitly out of scope? If they ask you for Spotify, you can't possibly design all of it in 60 minutes; that is the work of many teams over months or years. So the goal is to decide what you will focus on and what you will leave out, who the users are, and which platform we are building for.
  • Non-functional requirements: the constraints. How many daily or monthly active users? What is the read-to-write ratio? What are the latency requirements? And what downtime is acceptable, meaning the availability?

At the end, ask something like: "Do this scope and these requirements make sense before I move on to the high-level design?" If they say yes, good. If they give you a hint, you probably missed something in the requirements, so clarify it before you continue.

1.2Step two: the data and API layer 13:12

If it helps you to understand the entities and their relationships first, start here. If picturing the user flow from the top is easier, start with the high-level design.

The data model:

  • The core entities. The speaker's example: in YouTube they are Videos and Users.
  • The relationships between them, the primary keys, the foreign keys, the important indexes, and the rough size of each record.
  • Which data needs strong consistency and which can use eventual consistency.

The API contracts:

  • The core endpoints with their request and response.
  • It is good to talk about pagination, error handling, rate limiting, versioning, and REST best practices (since REST is the most common in most cases).

This is also where you choose the database (PostgreSQL, MongoDB, anything), but you must explain why based on the requirements, and go over the alternatives you considered and did not choose.

1.3Step three: the high-level architecture 15:03

This is the part where you spend a lot of time: you design the components and show how data flows through them. If nothing comes to mind, start with these four, because they exist in almost every system:

  1. API servers that receive the requests.
  2. A load balancer that distributes the traffic across them.
  3. A database that stores the data.
  4. A cache (optional) for data that is fetched often.

Then draw the user's journey from the frontend (web, mobile or desktop), to the API gateway and the load balancer, to the API servers, to the cache, to optional message queues that decouple the services, and finally the database. Walk the interviewer through the read and the write step by step.

  • If you use a cache, mention the TTL and the invalidation strategy.
  • For each part of the system, explain your choice between consistency, availability and partition tolerance (the CAP theorem), because different components may need different choices. This shows you understand the theory and the trade-offs.

(Explainer's note): The auto-generated subtitles say "CP theorem"; what is meant is the CAP theorem (Consistency, Availability, Partition tolerance), as the speaker spelled out its letters in the same sentence.

1.4Step four: bottlenecks, scale and reliability 16:57

5 to 10 minutes, depending on how much time you spent on the earlier steps. Here you show that you think about what breaks under load, not just the happy path, and this is one of the clearest signals that separates mid-level from senior:

  • Single points of failure: find them and propose a fix for each one.
  • Fallbacks: is there a slower path that still works if something breaks?
  • Redundancy: replicas across multiple availability zones, app servers that auto-scale across multiple AZs, and a distributed cache with replication.
  • Monitoring, logging and alerting: API latency, error rates, thresholds and alerts. Many people forget this part, but it is a strong signal that you think like a senior.

And ask the interviewer: are there specific failure scenarios you would like me to discuss?

1.5Step five: trade-offs and extensions 18:44

Usually an open question:

  • Summarize the key decisions: what you focused on, what you sacrificed, and why (be explicit about the reason).
  • Extensions: if you had more time, or the load grew 10x, what would you do?

And don't forget: no design is complete in 60 minutes. The goal is not for them to see a perfect design (it won't be), the goal is to show that you can evolve it.

BASICS

Dos and don'ts: how to run the conversation

2 min read 20:12

After the five steps, the speaker returns to the five dimensions and says what to do and what to avoid:

Do Don't
Ask clarifying questions before designing. This step is mandatory Jump to the solution before understanding the scope, which is an immediate red flag
Treat the interviewer as a colleague: two engineers at the same company working on a solution together Stay silent and draw diagrams without talking, or fill every silent moment with your own talk without asking for their input
Present more than one approach before committing to one Choose without explaining
Explain the trade-offs of any technology you choose Choose a technology without justification, which is another red flag
Check in at the end of each major phase (for example after the high-level design) Carry straight on without making sure nothing was missed

The speaker's example of "present more than one solution": in a payments system, consistency matters most, so choosing SQL is obvious. But if you choose it without explaining, the interviewer will either ask you "why SQL?" or not ask, and assume you don't know. So say why you did not choose NoSQL, for example, and what the trade-offs between them are.

The speaker's one-sentence summary: the interview is a conversation between engineers, not a monologue where you talk alone, and not a take-home assessment you solve without communicating.

INTERMEDIATE

Spotify: requirements and capacity estimation

3 min read 23:50

Spotify is a classic problem because it involves audio delivery, metadata management, object storage, and everything you need to design from scratch. The speaker says it is very useful for mid-level engineers who struggle with designing systems from scratch. The plan: requirements and capacity, then the high-level architecture and the read and write paths, then the API and storage, then scaling to 50 million users, and finally the "pyramid" that separates a senior from a mid-level engineer.

3.1Requirements 25:08

Scale: about 500,000 users and 30 million songs.

Core requirements:

  • Artists can upload their songs.
  • Users can search for songs and play them.
  • They can create and manage playlists, which requires a profile for each user.
  • Basic monitoring and observability: server health checks, error tracking and performance metrics.

Audio formats: Ogg and AAC, because they compress audio without much loss of quality and take less space.

Quality Bitrate
Mobile data saving 64 kbps
Normal quality 128 kbps
Premium 320 kbps

The average song size at 128 kbps we assume to be 3 MB, but this is also one of the things you must clarify before you continue.

3.2Capacity estimation 26:35

Item Calculation Result
Song storage 3 MB × 30 million 90 TB of raw audio. You need to count it ×2 or ×3, because this excludes the replicas in different regions and the copies made when an artist re-uploads a song
Song metadata (title, artist reference, duration, file links...) 100 bytes × 30 million 3 GB, practically nothing
User metadata (profile, preferences, playlists) 1 KB × 500,000 0.5 GB
Daily bandwidth Average listen of 3.5 minutes at 128–160 kbps, so about 3–4 MB per play, and each user listens to 10–15 songs a day Large egress costs
Spotify storage (the speaker's numbers)
30M songs×3 MB=90 TB raw audio
90 TB×2–3 (regional replicas, re-uploads)=180–270 TB
30M songs×100 bytes=3 GB song metadata
500K users×1 KB=0.5 GB user metadata

Audio dominates storage and bandwidth; metadata is almost nothing.

Egress costs are the money the cloud provider charges when data leaves its network and reaches the users.

The key conclusion: audio dominates both storage and bandwidth, and metadata is a very small part of the system.

(Explainer's note): The speaker did not compute the total bandwidth figure. If we continue with his numbers: 500,000 × (10–15 songs) × (3–4 MB) ≈ 15–30 TB per day, which shows why he says egress will be a large cost.

INTERMEDIATE

Spotify: architecture and the read and write paths

3 min read 29:20

Components:

Component Its role
Mobile/Web app The interface: search, playback controls and playlist management. It makes HTTP calls to the REST APIs
Load balancer Distributes requests across multiple API servers using round-robin or least-connections, and runs health checks to make sure the servers are up
API servers RESTful APIs that authenticate the user with a JWT and fetch song metadata and links from the database
SQL database Metadata and user profiles
Blob storage (such as AWS S3) Audio files, since they are the largest part of the system. They are read through signed URLs, temporary links that expire after a while, so the link does not stay available forever

Why blob storage? Because it gives you practically unlimited scalability, built-in durability, and a reasonable cost for large files.

flowchart LR
    App[Mobile / Web app] --> LB[Load balancer<br/>round-robin or least-conn]
    LB --> API[REST API servers<br/>JWT check]
    API --> SQL[(SQL<br/>metadata)]
    API -.->|signed URL| Blob[(Blob storage<br/>S3 audio)]
    App -->|stream chunks| Blob
    CDN[CDN<br/>artist pages, metadata] --> App

(Explainer's note): The diagram's layout is mine; the components are from the speaker. The CDN appeared in his write path (section 4.2) and in scaling (section 6).

4.1The read path: a user plays a song 31:17

  1. The user taps Play, and the app sends GET /songs/{id}.
  2. The load balancer routes the request to an API server.
  3. The API server validates the JWT first.
  4. If the user is authenticated: it fetches the song's metadata from the SQL database.
  5. It generates a signed URL for the song and returns the metadata and the signed URL to the app.
  6. The app makes range requests for chunks of the audio to the blob storage using the signed URL, and the blob storage streams the audio.
  7. For analytics: POST /songs/{id}/play with the metadata you want to record.
  8. If the JWT is invalid, none of the above happens and an authentication error is returned.
sequenceDiagram
    participant App
    participant LB as Load balancer
    participant API as API server
    participant DB as SQL
    participant Blob as Blob storage
    App->>LB: GET /songs/id
    LB->>API: route
    API->>API: validate JWT
    API->>DB: get song metadata
    DB-->>API: song details
    API->>API: generate signed URL
    API-->>App: metadata + signed URL
    App->>Blob: range requests for chunks
    Blob-->>App: audio stream
    App->>API: POST /songs/id/play for analytics

4.2The write path: an artist uploads a song 33:01

  1. The app sends a POST with the song and its metadata, and the load balancer forwards it to an API server.
  2. The API validates the file's format and size (there are usually file size limits). If it is not acceptable, it returns a rejection: unsupported format or size.
  3. If it is fine: the file is stored in the blob storage first.
  4. Then we extract the audio metadata (duration, bitrate), add what came in the body (the title, for example), and insert it into the SQL database.
  5. We can organize the files hierarchically: artist/{artistId}/album/{albumId}/.... The speaker points out that object storage has no real folders; it is just an illusion, but useful for organization.
  6. If there is a CDN (caching artist pages, album metadata and song details close to the user), we need a CDN invalidation so users see the latest songs and updates right away.
  7. We return a response to the app that the upload succeeded.
sequenceDiagram
    participant App
    participant API as API server
    participant Blob as Blob storage
    participant DB as SQL
    participant CDN
    App->>API: POST song + metadata
    API->>API: validate format and size
    alt invalid
        API-->>App: rejected - unsupported format or size
    else valid
        API->>Blob: store audio file
        API->>API: extract duration, bitrate
        API->>DB: insert song metadata
        API->>CDN: invalidate stale cache
        API-->>App: upload successful
    end
INTERMEDIATE

Spotify: the API and storage design

3 min read 35:19

5.1API design 35:27

Purpose Endpoint Notes
Search GET /search?q=...&type=song&limit=20&offset=0 type if we know the user is searching for a song. limit defaults to 20, and offset is where counting starts
Trending songs GET on the trending endpoint, with optional filters (such as genre) and limit The path is unclear in the auto-generated subtitles
A specific artist's songs GET /artists/{id}/songs?limit=...
A song's metadata and audio link GET /songs/{id} Returns the metadata and the streaming URL
Direct streaming GET /songs/{id}/stream An alternative to signed URLs
Playlist GET /playlists/{id}?include=songs Without include it returns only the playlist details
Create a playlist POST /playlists { name, public }
Add songs PUT /playlists/{id}/songs The song IDs and the position (if there are 5 songs, we add from position 6, for example)
Remove a song DELETE /playlists/{id}/songs/{songId}
Like / Unlike POST /songs/{id}/like Keeps the favorites in the DB
The current user's playlists GET /users/me/playlists?limit=...
Liked songs GET /users/me/likes?limit=50&offset=... The speaker says to use the same plural form across all endpoints, to follow REST principles
Follow an artist POST /users/me/follow Includes artistId, so the user gets their new releases

(Explainer's note): The speaker said there is a trending endpoint with optional filters, but the path itself is lost in the auto-generated subtitles, so I won't invent one.

5.2Storing the audio files 40:00

  • Blob/object storage such as S3 on AWS or Azure Blob Storage, because these large files can't be stored in SQL or a simple database.
  • The files are immutable: you can't modify them after upload. If you want a change, you upload a new file and delete the old one.
  • Hierarchical organization: artist/{artistId}/album/{albumId}/{songId}, which makes the files easier to access later.

5.3The schema, and why SQL 40:52

The database: PostgreSQL or MySQL. The minimum set of tables:

Table Core columns Relationship
users id, email, password_hash, created_at, last_login, subscription_type, country users.id ← playlists.owner_id (FK)
playlists id, owner_id, name, created_at Every playlist has an owner
playlist_items (join table) playlist_id, song_id, position, added_at Links playlists to songs
songs id, title, duration, ..., file_url file_url is what we use to fetch the audio from the blob storage
artists id, name, country, ... A separate table from users
artist_songs (join table) artist_id, song_id Which song belongs to which artist

Why SQL? Because we need many joins (playlists with songs, songs with artists), and complex queries such as "get all the songs of the artists this user follows". So SQL is the natural choice here.

Indexes, once the DB grows, on the columns that are read often:

  • songs.title
  • artists.name
  • playlist_items.song_id, which speeds up fetching a given playlist's songs, or finding which playlists contain a given song.

(Explainer's note): The speaker said these indexes will give us fast search "that handles typos and partial matches". A regular SQL index does not do that on its own. That is exactly why, in the Booking.com example, he uses Elasticsearch for search (section 10.4).

INTERMEDIATE

Spotify: scaling to 50 million users and the senior pyramid

3 min read 44:34

The estimates after growth:

Item Calculation Result
Song metadata 100 bytes × 200 million 20 GB
User metadata 1 KB × 50 million 50 GB
Audio 3 MB × 200 million about 600 TB, and in reality ×2 or ×3 with the copies in regions and repeated uploads

So the current architecture won't hold. The scaling steps:

  1. CDN edges in every region: North America, Europe, South America... so fetching is faster.
  2. Load balancers in multiple regions (at least two in two regions) routing to the API servers.
  3. A CDN eviction policy: LRU, meaning when the space fills up it removes the least recently requested content, and on a cache miss it fetches from the origin storage.
  4. Stateless API servers: we keep no session or storage on them, so we can add as many as we need just by making copies of them. You must design them stateless from the start for this to be possible.
  5. Separate reads from writes in the database: writes (new songs, new playlists) go to a leader, and reads (song metadata, playlists) come from followers kept in sync with it.
  6. Sharding if that is not enough: a shard for US users and a shard for European users (geographic partitioning), and songs split by ranges of the artist ID. This gives horizontal scalability and improves performance because related data stays together.
  7. Blob storage in every region: European users upload to storage in Europe, North America to its own storage... and all of them still use signed URLs.
flowchart TB
    U[Users per region] --> CDN[CDN edges<br/>NA / EU / SA, LRU]
    U --> LB1[LB region 1] & LB2[LB region 2]
    LB1 & LB2 --> API[Stateless API servers x N]
    API -->|writes| L[(Leader DB)]
    L -.->|sync| F1[(Follower)] & F2[(Follower)]
    API -->|reads| F1 & F2
    API -.-> B[(Regional blob storage)]

(Explainer's note): The diagram's layout is mine. The speaker said there are other things that could be improved (reliability, monitoring), but the core idea is: start simple and scale gradually.

6.1The pyramid that separates a senior from a mid-level engineer 48:49

The speaker compares design to a pyramid:

Position in the pyramid Phase What it contains
The base (largest and most important) Planning / Blueprinting Understanding the requirements, estimating capacity, and choosing the architecture
The middle Tech selection SQL or NoSQL, and if SQL, which one exactly; cache layers, eviction policies and storage solutions
The top (smallest) Coding The implementation, which takes the least mental effort

And if code is the only thing you know how to do, models such as Claude and GPT do it faster, better and cheaper, so why would a company pay you? This is where most mid-level engineers are stuck: their pyramid is upside down; they start writing code with no plan and no technology selection and see where they end up. Those who learned to think through the problem before touching the keyboard will never struggle to find work, because companies always need someone who thinks from the user's perspective and turns unclear ideas into something real.

INTERMEDIATE

Booking.com: requirements and load estimates

4 min read 51:15

The mistake most developers make is treating this system as an ordinary CRUD app with some caching and replication. But there is a moment when two users try to book the last available room at the same time, and the naive approach (SELECT then INSERT) causes a double booking: you charge two cards for one room, then refunds, angry customers, and a system nobody will trust again.

That is why this system tests the skills that separate mid-level from senior: concurrency without the system collapsing under load, consistency of data across multiple systems, and designing for failure. The goal is not to memorize patterns; it is to understand the real problems, their solutions and the trade-offs between them.

7.1Clarifying questions 53:38

Question Assumed answer What it affects
Is payment on the platform, or at the hotel at check-in? On the platform We need a payment system inside
A website only, or a mobile app too (possibly with different prices)? Both
Are there cancellations? Is there overbooking? Yes to both Hotels allow overbooking because they know there will be cancellations: for example, out of 10 bookings a month, 2 or 3 get cancelled
Fixed prices, or dynamic pricing? Dynamic Prices rise with demand. The speaker says the room he is recording from cost 3 times its normal price because it was the Christmas period

7.2Functional requirements 55:30

  • Hotel management: an admin panel to add, edit and delete hotels. The real heart of the system is users booking and cancelling, but we will assume there is an admin panel.
  • Search and discovery: by location, dates, number of guests, price range and other filters.
  • Booking: book a room, view your bookings, cancel, and the system prevents double booking.
  • Payment: integration with Stripe, PayPal or others.
  • Notifications (optional): confirming a booking, cancellation or payment to the user and to the admins or the brokers who list the hotels.

The speaker's way to identify the core: ask yourself "if I removed this feature, would the system still be Booking.com?" If the user can't search for hotels by location, the app is useless, so location search is core.

7.3Non-functional requirements 57:25

Requirement Value
Consistency Strong for booking (two people can't book the same room for the same dates), and eventual is acceptable for search and discovery
Concurrency Thousands of users trying to book the same hotel at the same time without the system collapsing
Latency Search results under 500 ms, and booking confirmation is acceptable within 2–3 seconds
Availability 99.9% (three nines)
Scale Horizontal scaling without redesigning the system from scratch

(Explainer's note): The auto-generated subtitles say "free 9 uptime"; what is meant is "three nines", i.e. 99.9%.

7.4Load estimates (capacity math) 58:44

The two most important numbers: QPS (queries per second) and TPS (transactions per second, i.e. booking writes to the DB).

The funnel: 100% of the traffic browses, about 10% of them search and compare, and only about 10% of those actually book. The rest are tire kickers who just look around, but we still have to design for them because they drive up the QPS.

Two ways to estimate: get one number from the interviewer and work from it:

  • Top-down: monthly traffic (for example from a tool such as SimilarWeb): 500 million visits a month.
  • Bottom-up: bookings per year: 1.1 billion (room nights).

We also need the average length of stay: assume two nights per booking.

Writes (TPS):

Step Calculation Result
Room nights per year Given 1.1 billion
Bookings per year 1.1 billion ÷ 2 nights 550 million
Bookings per day 550 million ÷ 365 about 1.5 million
Bookings per second 1.5 million ÷ 86,400 about 17 TPS

Reads (QPS):

Step Calculation Result
Visits per month Given 500 million
Visits per day 500 million ÷ 30 about 17 million
Visits per second 17 million ÷ 86,400 about 200
Backend reads per visit About 8 pages per visit, so about 5 reads (search, availability check...) ×5
Reads per second 200 × 5 about 1,000 QPS

The conclusion: 1,000 QPS versus 17 TPS, a strongly read-heavy system. The challenge: optimize the architecture for reads while keeping write consistency so double booking never happens.

(Explainer's note): The subtitles say "divide by 40, the days of the month" (possibly a mishearing of "thirty"); the correct figure is 30. And indeed 500 million ÷ 30 ≈ 17 million, which is the number he carried forward, so the slip is in the wording only, not the math. In the intro, the subtitles wrote "1,00 queries per second"; what is meant is 1,000.

INTERMEDIATE

Booking.com: architecture, the API and preventing double bookings

6 min read 64:16

The speaker starts with a service-oriented architecture with clear boundaries, but keeps tightly related data in the same database.

8.1Components and services 65:24

Component Its role
Client (mobile or web) Users browse and book
CDN Static assets: images, media, JavaScript files and hotel photos
API gateway The single entry point: authentication, rate limiting, routing to the services, and load balancing between instances of the same service
Hotel service Hotel and room data. Mostly static and can be cached heavily
Reservation service Booking logic, inventory checks, creating and cancelling bookings. This is where concurrency is handled
Rate service Room pricing by date: dynamic pricing based on demand
Payment service Integration with Stripe, PayPal, Apple Pay, Google Pay and others
Notification service Talks to Reservation and Payment, and sends emails and push notifications
PostgreSQL For ACID: we will run many transactions and need consistency
Redis A cache for data that doesn't change often

8.2Improvements: Elasticsearch and the message queue 67:18

  • 1,000 read QPS is a lot for PostgreSQL, so we add a search service on top of Elasticsearch: full-text search over hotel names and descriptions, geographic filtering (for example within 5 kilometers of a place), by price, rating and any other filter. It is much faster than PostgreSQL at this, and eventually consistent: it is updated from the DB after a short delay.
  • A database per service (Payment DB, Hotel DB...) if we want to separate them further (pros and cons in 8.3).
  • A message queue (Kafka or RabbitMQ): when payment succeeds in the payment service, we publish a message to the queue, so: 1. The notification service sends an email or push to the user and to the hotel owner that the payment succeeded and the hotel is booked. 2. The search service updates availability in Elasticsearch (eventually).

So a user might search right after the payment and still see the hotel as "available", but they won't be able to book it, because booking is confirmed against the real hotel database.

flowchart LR
    C[Mobile / Web] --> CDN[CDN<br/>static assets]
    C --> GW[API gateway<br/>auth, rate limit, routing]
    GW --> H[Hotel svc]
    GW --> R[Reservation svc]
    GW --> RT[Rate svc]
    GW --> P[Payment svc]
    GW --> S[Search svc]
    H & R --> PG[(PostgreSQL<br/>hotels + reservations)]
    H -.-> RD[(Redis cache)]
    S --> ES[(Elasticsearch)]
    P --> Q[[Kafka / RabbitMQ]]
    Q --> N[Notification svc]
    Q --> S

(Explainer's note): The diagram's layout is mine. I put Hotel and Reservation on the same PostgreSQL because that is the solution the speaker arrives at in 8.3.

8.3Monolith, microservices, or something in between? 69:56

A single monolithsimple transactions, hard to scale
Pragmatic microservicesseparate services, one shared DB for related data
Full microservicesfull independence, distributed transactions
Option Advantage Drawback
A single monolith Simpler transactions Hard to scale or deploy on its own, and with hundreds of developers working on it they step on each other
Full microservices (a DB per service) Full independence Distributed transactions and consistency problems that don't exist in a monolith
Pragmatic microservices Separation at the application level, with tightly related data (reservations and hotels) in one shared DB You use local ACID transactions instead of distributed ones

8.4API design 71:40

REST for all operations. The speaker mentions a common alternative: a GraphQL layer in front of the microservices: the frontend talks only to GraphQL and knows its schema, and GraphQL talks to the services behind it. But to keep the diagram simple, he assumes the frontend talks to the services directly.

Who logs in? Admins must log in. Users can browse without logging in, but when they come to book they must create an account or log in.

Operation Endpoint Reads/writes where
List hotels with filters GET /hotels?... Elasticsearch
A specific hotel GET /hotels/{id} PostgreSQL
Add a hotel (admin) POST /hotels Insert into SQL, respond 201 Created, then it reaches Elasticsearch after a short delay
Edit a hotel PUT or PATCH /hotels/{id} SQL
Delete a hotel DELETE /hotels/{id} SQL, then removed from Elasticsearch
The hotel's rooms (sub-resource) GET /hotels/{id}/rooms and GET /hotels/{id}/rooms/{roomId} SQL
Add / edit / delete a room POST /hotels/{id}/rooms, and PUT and DELETE /hotels/{id}/rooms/{roomId} SQL
My booking history GET /reservations SQL
Booking details and invoice GET /reservations/{id} SQL
New booking POST /reservations SQL (details in 8.5)
Cancel DELETE /reservations/{id} Checks that cancellation is allowed: a week before is fine, last minute usually not. If not allowed, it returns a response saying so and shows the options
Change the dates PUT /reservations/{id} SQL
Search GET /search?location&checkIn&checkOut&guests&beds&priceRange... Always Elasticsearch, not the DB, since that is why it exists in the first place

8.5Preventing a duplicate booking from the same user 77:50

There are two double-booking scenarios:

  1. The same user clicks "Book" twice (a double click). We solve this here.
  2. Two different users book the same room. That is in section 9.5.

The first solution (the simplest): the frontend generates a reservation ID (UUID) once for this booking and sends it with POST /reservations.

  • The server looks up the reservation ID: if it doesn't exist, it inserts, returns the confirmation, and Kafka publishes a message so the notifications reach the user and the hotel owner.
  • If it exists (a repeated attempt), it returns the existing booking and shows the user it is already booked.

The second solution (on the backend): a hash of hotel_id + room_id + user_id + a date or timestamp. The date matters, because without it you would block the user from booking the same hotel again in the future. If the hash exists in the DB, you block the booking.

The speaker prefers the UUID from the frontend: much lighter and simpler than storing a hash and checking it on the backend every time, as long as we are talking about a single user repeating a request.

sequenceDiagram
    participant FE as Frontend
    participant RS as Reservation svc
    participant DB as PostgreSQL
    participant K as Kafka
    FE->>FE: generate reservation ID once
    FE->>RS: POST /reservations with reservation ID
    RS->>DB: reservation ID exists?
    alt new
        RS->>DB: insert reservation
        RS-->>FE: confirmation
        RS->>K: publish booked event
    else duplicate
        RS-->>FE: return existing reservation
    end

(Explainer's note): The speaker didn't say the word "idempotency", but this reservation ID is exactly an idempotency key: the same request sent twice gives the same result without a second booking. The name comes from the chapter title in the video.

ADVANCED

Booking.com: the data model, inventory and concurrency

6 min read 82:56

9.1The tables 83:04

An important point: you book a room type (for example Double queen), not a specific room; the room number is assigned at check-in.

Table Columns Relationship
hotels id, name, address, location, description Hotels under the same brand: either the same name with different IDs (a company in the US and a company in Europe), or a helper table hotel_groups
room_types id, name, description, capacity, hotel_id One hotel → many types (one-to-many)
room_type_rates The price for each type One type → many rates. Example: basic 100 a day, with a better view 150, with breakfast 200
rooms id, floor, is_available, room_type_id, hotel_id The physical room assigned at check-in. hotel_id is here for convenience, even though you could reach it with a join
room_type_inventory hotel_id, room_type_id, date, total_inventory, total_reserved Details in 9.2
reservations id, room_type_id, hotel_id, guest_id, start_date, end_date, status (Reserved, Canceled...) One guest → many reservations
guests id, first_name, last_name, email... And possibly an admins or brokers table for whoever lists the rooms
transactions id, reservation_id, type (New, Change, Refund), amount (positive or negative) One reservation → many transactions

Why are transactions in their own table? Because a booking goes through stages: it is booked (the first transaction), then the user may change the dates so there is a price difference to pay or refund (a second transaction), or cancel so there is a refund with a negative amount (a third transaction).

9.2Room type inventory 86:34

Inventory is tracked per hotel, per room type and per day, not per physical room. This gives us:

  • Bulk availability updates.
  • Pricing and search that scale.
  • Simple overbooking logic.

There is a row for each (hotel, room type, date). We pre-populate it two years ahead, and a daily cron job adds new dates as time passes.

hotel_id room_type_id date total_inventory total_reserved
211 1001 Dec 15 100 80
211 1001 Dec 16 100 92
211 1001 Dec 17 100 87

(Explainer's note): The speaker showed sample data for December 15, 16 and 17 without reading out every number. The IDs and the total_reserved figures in this table are a (suggested example), except that 100, 92 and 87 were mentioned in the explanation (100 and 92 in the December 23–27 availability example, 87 in the cache section), not tied to these dates, so assigning them to December 16 and 17 is also a suggested example.

9.3Availability checks and overbooking 90:33

The speaker's example: a booking from December 23 to 27:

SELECT date, total_inventory, total_reserved
FROM room_type_inventory
WHERE hotel_id = :hotel_id
  AND room_type_id = :room_type_id
  AND date BETWEEN '2025-12-23' AND '2025-12-26';

And the condition for each day:

total_reserved + rooms_to_book <= total_inventory

Example: total_inventory = 100 and total_reserved = 92, so 8 are left. The user wants one room: 92 + 1 = 93 ≤ 100, so the booking is allowed.

With overbooking: if we allow 10%, we multiply the inventory by 1.1, so with 100 rooms we allow bookings up to 110, because we expect about 10 to be cancelled ahead of time:

total_reserved + rooms_to_book <= 1.1 * total_inventory

(Explainer's note): The speaker said "Select... where the date is between the range"; writing the SQL in this form, and the year in the date, are mine. I made the last day the 26th because the night of the 27th is the check-out day, which is my assumption, not the video's.

9.4Why a relational database here? 92:09

  1. ACID, and especially consistency, to prevent double booking.
  2. The system is read-heavy (1,000 QPS versus 17 TPS), relational DBs handle reads well, and we can add Elasticsearch for search.
  3. Clear relationships: rooms, hotels, types, reservations and transactions.
  4. Complex queries: joins between inventory and rates, filtering by dates, and aggregating totals.

9.5Concurrency and race conditions 93:26

The first scenario (the same user twice): solved by the reservation key. The second attempt gets a duplicate key error, so we return the existing booking.

The second scenario: two different users, and the room type has one spot left.

The default isolation level is read committed: you see data only after other transactions commit. But there is a gap between when you read (check availability) and when you write (book):

sequenceDiagram
    participant A as User A tx
    participant DB as Inventory row
    participant B as User B tx
    A->>DB: read total_reserved = 99 of 100
    B->>DB: read total_reserved = 99 of 100
    A->>A: 99 + 1 fits in 100, OK
    B->>B: 99 + 1 fits in 100, OK
    A->>DB: book, reserved = 100
    B->>DB: book, reserved = 100 (2 bookings)
    Note over DB: double booking

Both read correct data and passed the check, and then each one wrote over the other. That is why read committed alone is not safe for bookings.

(Explainer's note): The speaker says the two transactions "override each other". If each writes the value it computed, both write 100 (a lost update) and two bookings exist for one room, which is what the diagram shows.

9.6The two solutions 95:01

Serializable isolation Read committed + row locking (SELECT ... FOR UPDATE)
How Set the transaction's isolation level to serializable in code Lock the relevant inventory rows while you check and book
Pros Simple to implement, and fine at low TPS Only whoever tries to lock the same row waits. Everything else runs in parallel
Cons Transactions block each other, so it doesn't scale at high TPS. And deadlocks are possible if you lock several resources in different orders —
Mitigation Lock timeouts so there is no endless waiting, and short transactions

(Explainer's note): "Transactions block each other" is the speaker's description. In PostgreSQL, Serializable (SSI) does not queue transactions; it aborts conflicting ones with a serialization error, and the application must retry them. Either way the cost grows with TPS.

The speaker considers the second solution the better one: FOR UPDATE prevents the race condition by serializing access to those specific rows, and read committed stays PostgreSQL's default. Serializable is acceptable if the TPS is low.

BEGIN;
SELECT total_inventory, total_reserved
FROM room_type_inventory
WHERE hotel_id = :h AND room_type_id = :rt
  AND date BETWEEN :start AND :end
FOR UPDATE;
-- check total_reserved + n <= total_inventory, then:
UPDATE room_type_inventory SET total_reserved = total_reserved + :n
WHERE hotel_id = :h AND room_type_id = :rt AND date BETWEEN :start AND :end;
INSERT INTO reservations (...) VALUES (...);
COMMIT;

(Explainer's note): The speaker named SELECT ... FOR UPDATE, but this full code (suggested example) shows where the lock happens.

ADVANCED

Booking.com: sharding, caching, search and replication

6 min read 97:09

10.1Sharding by hotel_id 97:18

With 1,000 QPS and 17 TPS, you will eventually need sharding, and it will show in how long queries take. The most important decision is the shard key.

The speaker's proposal: hotel_id. Why?

  • Most queries filter by hotel first (is its location suitable for the search?) and then fetch the room types.
  • All of a hotel's data stays on the same shard, so there are no cross-shard joins.
  • Local transactions within the shard.

To start: modulo sharding:

shard = hash(hotel_id) % number_of_shards

This is fine when you know the number of shards won't change much. Example: 1,000 QPS over 16 shards is about 62 QPS and about 1 TPS per shard.

10.2Modulo or consistent hashing? 99:07

  • Modulo: if you add a 17th shard, you have to redistribute almost all the data in the 16 old ones.
  • Consistent hashing: moves only about 1/17 of the data, the part that will be stored on the new shard.

How? A hash ring: servers and keys are placed as points on a circle, and each key goes to the nearest server clockwise. When you add a new server, only the keys between it and the server before it are affected.

The rule: a small, fixed number of shards means modulo. If you will add many shards, use consistent hashing.

flowchart LR
    K1((key A)) -->|clockwise| S1[Server 1]
    K2((key B)) -->|clockwise| S2[Server 2]
    K3((key C)) -->|clockwise| S17[New server 17]

(Explainer's note): The speaker drew the ring on the whiteboard. The diagram here is my simplification of the idea. He also pointed to a video on consistent hashing in his System Design course.

10.3Caching strategy 100:28

We cache the "hot" inventory data in Redis or Memcached:

Value
Key hotel_id + room_type_id + date
Value total_inventory (for example 100) and total_reserved (for example 87)
Overbooking rate (10%) Not in the cache: a constant stored elsewhere that enters the calculation
TTL about 5 minutes
flowchart TB
    S[User searches] --> C{Redis hit and TTL valid?}
    C -->|yes| Show[Show inventory]
    C -->|miss or expired| DB[(Read DB)] --> Fill[Populate Redis] --> Show
    Show --> Book[User books]
    Book --> V[Validate + write in DB]
    V --> Inv[Invalidate affected cache keys]

The key point: the booking itself is always confirmed against the DB, and once it succeeds we invalidate the affected keys, so the next search sees that this room type is no longer available.

10.4Search with Elasticsearch 103:06

Elasticsearch is a search and analytics engine, a database specialized in search, not for transactional workloads like PostgreSQL and MySQL. SQL is excellent at "get the row where id = 123" or "do an insert", and Elasticsearch is excellent at "find all the documents similar to this text, rank them by relevance, filter by several conditions, and do it fast across millions of documents". Example: hotels in Georgia, priced 80 to 180 a day, rated 8+.

Why not SQL? If you run SELECT * FROM hotels WHERE location LIKE '%Paris%' AND amenities LIKE '%WiFi%' AND description LIKE '%luxury%' ...:

The problem in SQL Elasticsearch
LIKE '%...%' does a full table scan, and indexes don't help, even if Paris is less than 10% of the table An index designed for this kind of search
No ranking by relevance It splits "luxury hotels in Paris" into tokens, finds the documents, and ranks them by relevance
It doesn't handle typos (if you type "Pari", no results) It detects the typo: edit distance = 1
Weak at geographic distance calculations "Hotels within 5 kilometers of Paris"; a small town nearby might be cheaper
Aggregations are expensive Fast

How is the data stored? Yes, we duplicate the data. In SQL the hotel is spread across tables (hotel, room types, amenities), and Elasticsearch turns it into one JSON document with the hotel_id, name and address, with the location and room types nested inside. No joins, which is what makes it fast.

PostgreSQL is the single source of truth. Availability, overbooking and every transaction are confirmed against it. Elasticsearch is for search only.

10.5Syncing with CDC 108:17

To keep Elasticsearch eventually consistent with PostgreSQL:

flowchart LR
    PG[(PostgreSQL<br/>source of truth)] -->|write-ahead log| DBZ[Debezium connector]
    DBZ -->|one topic per table| K[[Kafka]]
    K --> C[ES consumer]
    C -->|update index| ES[(Elasticsearch)]
  1. PostgreSQL writes every change to the write-ahead log (WAL).
  2. Debezium (an open-source CDC tool) reads that log in real time.
  3. It publishes the changes to Kafka, with one topic per table.
  4. A consumer reads the events and updates the index in Elasticsearch.

It also helps with crash recovery: even if PostgreSQL goes down, you can read from the transaction log.

(Explainer's note): The auto-generated subtitles say "bases connector" and "dbium"; what is meant is the Debezium connector.

10.6Database replication 109:42

Sharding splits the tables (for example geographically), and replication scales reads: a primary for writes, and read replicas in different availability zones, with each user connected to the nearest replica.

Reading after a recent write: in the query for a given user's reservations, a variable decides whether to use the primary or not:

  • If there was a recent write, read from the primary, because the replica may not be updated yet.
  • Otherwise, read from the nearest replica.

10.7Global load balancing 111:09

Users come from the US, Europe, Asia and elsewhere, so the sensible algorithm is geo-latency routing by region. There are exceptions (an American travelling in Europe), but in general a European user searches for hotels in Europe.

In each region:

  • Read replicas of the DB.
  • A region-specific Redis (there is no point caching European hotels in the US region).
  • A region-specific Elasticsearch.

But there is one primary (even if it is a shard, for example shard 7), to avoid conflicts from several regions writing to the same data. This works because the TPS is low anyway (17), so one primary per shard and many replicas across regions is enough.

flowchart TB
    subgraph US[US region]
      R1[(Read replica)]
      C1[(Redis)]
      E1[(Elasticsearch)]
    end
    subgraph EU[EU region]
      R2[(Read replica)]
      C2[(Redis)]
      E2[(Elasticsearch)]
    end
    P[(Single primary<br/>per shard)] -.->|replicate| R1 & R2
    W[All writes] --> P

(Explainer's note): In the intro the speaker said he would talk about failures under load and recovery in the hotel system, but this part ends at replication and global routing without a section on failures. Failures are covered in detail in WhatsApp (section 15).

ADVANCED

WhatsApp: scale, architecture and real-time connections

6 min read 113:10

Messaging apps come up a lot in senior and staff interviews because they force you to deal with: stateful connections, real-time delivery, consistency, scale, failures of parts of the system or full disaster recovery, and multi-device sync (mobile and desktop). What is expected is that you show you understand not just the happy path, but also everything that can break while messages fly between users who are not connected over mobile networks.

The plan: a simple working version for a few thousand users, then scale to billions, then find where it breaks and fix it properly.

11.1Requirements 114:45

Functional:

  • One-on-one messages.
  • Group chats of up to 100 participants.
  • Delivery when the user is online and also when they are offline.
  • Attachments: images, video and audio (voice notes).
  • Message states: sent, delivered and read.
  • Online/offline status and last seen.

Non-functional:

  • Latency under 500 ms when both are online.
  • Guaranteed delivery: messages must not disappear.
  • Billions of users and high throughput.
  • We don't keep messages on the server longer than necessary, so storage stays reasonable.
  • Resilient: if a component goes down (such as the server that is supposed to deliver the message), the user eventually receives it when the server comes back.

11.2Estimating scale (back-of-the-envelope) 116:24

Metric Number
Registered users 1 billion
Daily active users 500 million
Concurrent connections at peak 50 million
Messages per user per day 10–20
WhatsApp scale (the speaker's numbers)
500M daily users×20 messages=10B messages/day
10B÷86,400 s=~115K writes/s
115K×3–5 at peak=~345K–575K writes/s (speaker rounds to 350K–500K)
10B×1 KB=10 TB/day
50M connections×10 KB/s=500 GB/s
Calculation Result
Daily messages: 500 million × 20 10 billion messages, i.e. 10 billion DB writes per day
Per second (average) about 115,000 writes/second
At peak (3–5x) about 345,000–575,000 writes/second (the speaker says 350,000–500,000)
Message size with metadata (text, sender and recipient IDs, timestamp, status flags) 1 KB
Daily storage: 10 billion × 1 KB 10 TB/day
Yearly storage about 3.6 PB/year (a PB = 1,000 TB)
Bandwidth: 50 million connections × 10 KB/s 500 GB/s

An important point: most messages are delivered and deleted from the server within seconds. Users download messages to their devices and back them up if they want (for example to Google Drive), and the server gets cleaned up. The speaker assumes 30-day retention for undelivered messages, and that about 10% of users keep their history on the servers. This is what lets WhatsApp run on little infrastructure.

(Explainer's note): 115,000 × 5 ≈ 575,000, and the speaker rounded the peak to 350–500 thousand. Both are rough estimates; what matters is the order of magnitude: hundreds of thousands of writes per second.

11.3High-level design 119:03

Component Its role
Mobile app Keeps a persistent connection to the chat server, the UI, local message storage, and retries if something fails
Load balancer Distributes traffic across many chat servers to handle the peak
Chat servers Hold a WebSocket with each client (a channel open in both directions), route messages, and track who is online
Message queue (Kafka or RabbitMQ) Decouples writing a message from delivering it: the server immediately tells the app the message was sent, then puts it on the queue asynchronously for storage and delivery
Message storage service + NoSQL (Cassandra or DynamoDB) Consumes messages from the queue and stores them. NoSQL because we need very high write throughput, and the queries are simple (by user, conversation or timestamp)
User connection cache (Redis) Who is online, which chat server they are connected to, and last activity. Redis takes microseconds versus milliseconds for the DB, and that difference shows at this scale
Blob storage (S3 or Google Cloud Storage) + CDN The files themselves, and the CDN caches popular files at edge locations around the world: a user in the US sent to a user in Europe, so the European downloads from the CDN nearest to them
Notification service For offline users: APNs for Apple and FCM for Android
Presence service Online/offline: receives heartbeats from users and updates from Redis, and publishes the changes to whoever is interested (user A wants to know whether B is online)
flowchart LR
    App[Mobile app] --> LB[L4 load balancer]
    LB --> CS[Chat servers<br/>WebSocket]
    CS --> Q[[Kafka / RabbitMQ]]
    Q --> MS[Message storage svc]
    MS --> DB[(Cassandra / DynamoDB)]
    CS --> R[(Redis<br/>who is where)]
    CS --> N[Notification svc<br/>APNs / FCM]
    CS --> P[Presence svc]
    App -.->|media| Blob[(Blob storage)] -.-> CDN[CDN]

(Explainer's note): The diagram's layout is mine; the components are from the speaker.

11.4Polling, long polling or WebSocket? 123:38

Pollingasks every few seconds, mostly empty answers
Long pollingholds the request, reconnects each time
WebSocketone persistent two-way connection
Method How it works The problem
Polling The client asks every so often "anything new?", and the answer is usually empty It wastes bandwidth and adds latency: if you ask every two seconds and the message arrives right after you asked, you wait two seconds. And millions of users ask even when there is nothing new
Long polling The server holds the request until a message arrives or the timeout expires, then the client asks again Opening and closing connections all the time, with a full HTTP handshake each time. Servers spend more time managing connections than delivering messages, and the waiting connections eat memory and thread pool slots
WebSocket One persistent, bidirectional connection: HTTP at first, then a WebSocket handshake, and after that it stays open for the whole session The server can send to the client at any time, and the client can send or acknowledge without a new connection

WebSocket's trade-offs:

  • It needs load balancers that support it: Layer 4 instead of Layer 7.
  • More memory per connection on the server, because it stays open.
  • But it is worth it for real-time delivery and the latency requirements.

11.5Managing the chat servers: service discovery 127:39

In production, chat servers crash, get deployed, and scale up and down. The load balancer has to know which ones are up (for example 1 and 2 are healthy, and 3 is throwing timeouts or errors).

The solution: a service discovery cluster (a service registry such as Consul or AWS Cloud Map):

  1. When a server comes up on a given IP, it registers itself in the registry.
  2. The registry runs a health check every 10 seconds on each server.
  3. The load balancer asks for the list of healthy servers, and the registry sends it (for example only 1 and 2).
  4. The load balancer updates its routing table and sends nothing to server 3.
ADVANCED

WhatsApp: the data model, message paths, devices and media

6 min read 129:28

12.1The tables 129:36

Table Where Core columns
users PostgreSQL or MySQL id (PK), phone_number (unique), username, created_at and last_seen
messages Cassandra (high volume) message_id (PK), sender_id, receiver_id, content, media_url, status (Sent/Delivered/Read), timestamp and is_deleted
groups PostgreSQL id (PK), name, created_at and created_by
group_members PostgreSQL group_id, user_id, role and joined_at (one group → many members)
User connections registry Redis user_id, server_id, connection_id, whether it is open, last_active and device_type. Updated about every 300 seconds with the heartbeat

(Explainer's note): The speaker says the registry is refreshed about every 300 seconds "with each heartbeat", but in 12.2 he puts the heartbeat at every 5–10 seconds. The two numbers don't match; treat both as rough examples.

12.2The API: WebSocket and REST 132:19

Over the WebSocket:

Direction Message Contents
Client → Server Auth user_id, the token and device info
Server → Client Auth success session_id, the server time and a count (probably of unread messages)
Client → Server Send message The recipient, the content, media if any, and the timestamp
Server → Client Ack That the message reached the server
Server → Client New message message_id, the sender, the content and media
Client → Server Delivered ack Received but not opened yet
Client → Server Read receipt message_id and the read time; the server forwards it to the sender
Both directions Presence heartbeat Every 5–10 seconds. If you close the app or the browser tab, the connection closes so we don't waste bandwidth

Media over REST:

  1. POST /api/media with the file type, size and conversation.
  2. The server responds with an upload URL (pre-signed), a media_id and an expiry time.
  3. The client uploads the file to the pre-signed URL directly on the media storage, which responds with upload success.
  4. The message is sent with the media, and the server acks it. If an error happens, it responds with message delivery failed and the reason in the body.

(Explainer's note): The auto-generated subtitles say "the onlet count" in the auth success response, and the word is unclear. The closest reading is "unread count", but that is my guess.

12.3Multi-device sync 135:35

The speaker's example: user A has 3 devices (mobile, tablet and desktop), and user B has two devices (mobile and laptop), and each device may be connected to a different chat server.

  1. A's mobile sends to server A, and the server replies with Sent.
  2. Server A asks Redis: "where is B?", and it replies that B has two devices, on server B and server E.
  3. The same message shows as Sent on A's tablet (through server C) and A's desktop (through server D).
  4. Server B delivers it to B's mobile, and server E to B's laptop.
  5. B's mobile acks, so server B notifies server A, and A sees Delivered.
  6. B reads it, so a read receipt goes to server B, then to server A, and A sees Read; it is also marked Read on B's laptop.

Conflicts: last write wins using version numbers.

Optimizations:

  • Incremental sync: a new device doesn't need the whole history. It requests the last 30 days, and older messages are fetched when the user scrolls up.
  • Very old messages live in cold storage and are lazy loaded.

The trade-off: about double the delivery work, because each message goes to more than one device. But it is a core feature, because users want the same conversations on mobile and desktop.

12.4How a message travels: online and offline 140:16

Case one: both are online

sequenceDiagram
    participant A as User A
    participant SA as Chat server A
    participant Q as Kafka
    participant R as Redis
    participant SB as Chat server B
    participant B as User B
    A->>SA: send via WebSocket
    SA->>SA: generate timestamp-based message ID
    SA-->>A: ack - sent
    SA->>Q: push for persistence
    SA->>R: where is B?
    R-->>SA: B is on server B
    SA->>SB: route via Redis pub/sub
    SB->>B: deliver in real time
    B-->>SB: delivery ack
    SB->>SA: forward delivery
    SA-->>A: status delivered
    Note over Q: queue writes to Cassandra

When B opens the message, the same steps repeat and the status becomes Read. Optionally, we put the message on the queue for analytics or archiving asynchronously.

Case two: B is offline

  1. The same start: A sends, and server A asks Redis.
  2. B is offline, so the message goes into B's inbox in Redis, and also into an inbox table in the DB.
  3. Server A calls the notification service, which sends a push through APNs or FCM.
  4. The message is put on the queue, so it gets stored in the DB.
  5. When B comes back online and opens a WebSocket with, say, server C: the server pulls the pending messages from the Redis inbox, and any missed messages from the DB inbox, delivers them, and notifies A of the status.

12.5Group messages (fanout) 145:02

The speaker's example: group 789 with about 50 members.

  1. A sends to the group on their chat server.
  2. The server asks the group service: who is in group 789? It returns the list of user IDs.
  3. It checks the status of the 50 in Redis (so it doesn't waste DB queries): 30 online and 20 offline.
  4. For the 30 online: it publishes on Redis Pub/Sub (a channel per user), so delivery happens in parallel. For example 15 on server B and 15 on server C, and each server delivers over the WebSocket. The acks return to server A so it can update the status for A.
  5. For the 20 offline: the same idea as one-on-one: it writes to the Redis inbox and 20 inbox records in the DB, and a push notification for each one.
  6. The message is stored only once: one write from server A to the queue, and the queue writes it to the DB.
  7. A sees: delivered to 30, pending for 20.

12.6Media: pre-signed URLs and a CDN 148:54

The wrong way: the client uploads the image to the chat server, the server stores it, and the recipient downloads it from the chat server. This is not scalable: the servers become bound by the bandwidth of moving files, and they are doing what they are bad at (moving large blobs) instead of what they are good at (routing messages quickly).

The right way:

sequenceDiagram
    participant A as App A
    participant SA as Chat server A
    participant S3 as Blob storage
    participant SB as Chat server B
    participant B as App B
    participant CDN
    A->>SA: request pre-signed upload URL
    SA->>S3: generate upload URL
    SA-->>A: upload URL
    A->>S3: direct upload
    S3-->>A: media URL
    A->>SA: chat message with media URL
    SA->>SB: deliver message
    SB->>B: message + media URL
    B->>SB: tap to view - request download URL
    SB-->>B: pre-signed download URL
    B->>CDN: download via CDN
    CDN-->>B: stream media

Downloading: two ways:

  • Directly from storage: simpler.
  • Through a CDN: extra steps, but the performance and cost gain is huge at this scale. So the CDN is usually the better solution.

Why does this matter? A single chat server holding 100,000 connections could go down from just 100 users uploading small files of a few megabytes at the same time. This way the chat servers move only small messages containing links, and the blob storage and CDN carry the heavy bandwidth.

TTL: pre-signed URLs expire, and the media itself has a TTL of about 30 days matching the message retention, after which it is deleted automatically.

ADVANCED

WhatsApp: event-driven design, batching, encryption and rate limiting

6 min read 154:09

13.1Kafka partitions and storage instances 154:26

To scale further, we decouple the servers from each other with a message queue such as Kafka:

  • The message storage service is stateless, so we run as many instances as we need.
  • Each instance takes partitions (for example 0–9, 10–19 and 20–29).
  • Each instance has a Kafka consumer that does batch writes to the database cluster.
  • On failure: retry after a timeout. If it still fails: we don't delete the message from the queue so it isn't lost, or we put it in a dead letter queue (a separate queue) and process it later.
  • After the write: an index updater updates the indexes for queries.

13.2The pipeline in detail, and batching 156:36

flowchart LR
    Q[[Kafka]] -->|batch of 500, poll every 100ms| C[Consumer]
    C --> TP[Thread pool<br/>20 workers]
    TP --> V[Validate schema<br/>+ size checks]
    V --> E[Enrich: dedup hash,<br/>mentions, links, indexes]
    E --> BW[Batch writer<br/>1000 msgs or 5 s]
    BW -->|sort by shard,<br/>group by conversation| DB[(Cassandra)]
    DB --> IU[Index updater] --> OK[Mark processed]
    BW -.->|failed| RT[Retry] -.->|still failing| DLQ[[Dead letter queue]]
  1. Batch size of 500 messages, and the consumer polls every 100 ms.
  2. Parallel processing with a thread pool of 20 workers.
  3. For each message: schema validation, and metadata checks (media size; if over the limit we respond with an error).
  4. Enrichment: a hash for deduplication, extracting mentions (@username) and links for the preview, and preparing the indexes.
  5. Batch writer: collects 1,000 messages or every 5 seconds in memory. When it is full: it sorts by target shard, groups by conversation, and prepares the batch.
  6. An atomic batch insert into the cluster, then the index updater, then marked as processed.
  7. If part of it fails (for example 10 out of 1,000): a simple retry first (maybe a rate limit or something minor), then the dead letter queue, with several retry steps (up to about 10) before we notify the user that the message wasn't sent.

Why batching?

Calculation Time
1,000 separate inserts 1,000 × 5 ms 5 seconds
One batch insert of 1,000 messages 50 ms
The difference about 100x throughput

He shows an example of a batch write in Cassandra:

BEGIN BATCH
  INSERT INTO messages (timestamp, sender_id, receiver_id, content, media_url, ...) VALUES (...);
  INSERT INTO messages (...) VALUES (...);
  -- ... up to 1,000 messages
APPLY BATCH;

(Explainer's note): This code is my condensed rewrite of what the speaker showed on screen. He described the batch as a "transaction".

(Explainer's note): The 5 ms and 50 ms figures are the speaker's illustration. In Cassandra, large batches that span many partitions are generally an anti-pattern for throughput; batching pays off when the batch is grouped by partition, which is why the writer sorts by shard and groups by conversation.

13.3End-to-end encryption 161:43

In WhatsApp, messages are encrypted on the sender's device and decrypted only on the recipient's device. Even the servers (A and B) can't read the content. The solution: the Signal protocol.

  1. Each user generates an identity key pair at registration: the public key is uploaded to a key server, and the private key never leaves the device.
  2. B also uploads prekeys, for example 100 signed prekeys, and the server stores them for anyone who will start a conversation.
  3. When A messages B for the first time: A fetches from the key server B's identity key and one of the prekeys.
  4. A generates a key pair and performs a key exchange, so both end up with the same session key.
  5. Each message is encrypted with the session key, along with a message counter that increments with every message. So even if the session key is stolen, decrypting every message is still very hard.

(Explainer's note): The counter claim is the speaker's. A counter alone does not protect messages once a session key is stolen; in the real Signal protocol that protection comes from the Double Ratchet, which derives a new key for every message.

sequenceDiagram
    participant A as User A device
    participant KS as Key server
    participant B as User B device
    A->>KS: upload public identity key
    B->>KS: upload public identity key + 100 signed prekeys
    A->>KS: fetch B identity key + one prekey
    A->>A: key exchange - derive session key
    A->>B: encrypted message + counter, via chat servers
    B->>B: decrypt on device

(Explainer's note): In one sentence the speaker said messages are "encrypted with A's public key and decrypted with A's public key". That is a slip of the tongue; in the Signal protocol both sides arrive at a shared session key from the key exchange and encrypt with it, which is what he said in the very next sentence. And a public key alone can't decrypt something encrypted with it.

13.4Rate limiting and abuse prevention 165:08

Without rate limits, any hacker could do scraping or a DDoS. So each action type has a per-user limit:

Action Counter Example limit
Sending messages Message token bucket 100 messages/minute
Creating groups Group create counter 5 or more per hour is a lot
Uploading media Media counter 10 or 100 files/minute
Connections IP connection counter 5 attempts in a minute from different IPs, which is suspicious
Group invites Group invite counter Configurable

The speaker says these numbers are examples, and they are decided during the blueprinting phase.

When the limit is exceeded: we block the action and tell the user ("Too many requests, try again in 60 seconds"), or ask for a CAPTCHA.

Progressive penalty:

  1. A warning.
  2. A one-hour restriction.
  3. A 24-hour restriction (like wrong password attempts).
  4. Account suspension until a human reviews it: a bot? A hacker? Or a user who didn't understand? They contact the user and reopen the account with the same limits, perhaps with a tag marking it as suspicious.
  5. If it repeats: it may be permanently deleted.

Token bucket (from the code he showed): each action has a capacity and a refill_rate, and on every request:

class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity, self.refill_rate = capacity, refill_rate
        self.tokens, self.last_refill = capacity, time.time()

    def allow(self):
        self.refill()                     # add tokens based on time since last_refill
        if self.tokens >= 1:
            self.tokens -= 1
            return True                   # continue: store message / create group
        return False                      # block, notify, maybe CAPTCHA

Why a central Redis? Each user is connected to a different server over the WebSocket, so the counters must live in a central store shared by all servers (Redis fits because we keep them for up to 24 hours). Atomic operations guarantee accuracy, and there is no way to do server hopping: connecting to another server in the hope that the limit doesn't apply there.

(Explainer's note): This code is my simplification of the logic of the code shown on screen (capacity, refill rate, tokens and last refill time), not a literal copy of it.

ADVANCED

WhatsApp: sharding and cleanup jobs

3 min read 172:10

14.1How do you choose the shard key? 172:36

Shard key Advantage Drawback
user_id All of a user's messages are on one shard, so reads come from one database Hotspots: some users use the app far more, so some shards are large and some small (imbalanced)
message_id Even data distribution Fetching one user's messages goes across all the shards and merges the results, so it is slow and expensive
Timestamp A third option the speaker mentioned

The speaker stresses that what the interview expects is not to just pick one, but to present the options and the trade-offs, and say which one you will go with and why.

14.2The solution: shard by user ID with duplicated writes 174:15

  • A's conversations are on shard A. When A talks to B (who is on shard B), we also write a copy to shard B.
  • The drawback: double the writes.
  • The advantage: very fast reads: each user reads only from their own shard.
  • Also, each shard has a replica, and each user reads history from the replica, so the primary is free for writes.
flowchart LR
    A[User A writes] --> SA[(Shard A primary)]
    A -->|duplicate write| SB[(Shard B primary)]
    SA -.->|async| RA[(Replica A)]
    SB -.->|async| RB[(Replica B)]
    RA -->|A reads history| UA[User A]
    RB -->|B reads history| UB[User B]

The rule: sharding (splitting the DB into smaller parts) improves writes, and replication improves reads.

The details: for example, user IDs 1 to 1,000 on shard 1. All writes go to the primary node, and from it async replication to 3 replicas (eventually consistent). Reads (opening messages, unread messages) come only from the replicas, and the primary handles writes only.

Interview tip: this trade-off (double writes for fast reads) has to be settled from the requirements: is the system optimized for writes or for reads? If you didn't ask at the start, ask now instead of guessing, and depending on the answer: replication, message ID sharding, or timestamp sharding.

14.3Cleanup jobs 178:02

The system fills up with garbage that will never be read again: expired media, old undelivered messages, and dead (stale) connections.

flowchart LR
    SCH[Job scheduler<br/>Airflow or cron, daily/weekly] --> JQ[[Job queue<br/>separate from message queue]]
    JQ --> WP[Worker pool<br/>containerized]
    WP -->|ack or requeue on failure| JQ
    WP --> T1[Delete expired media]
    WP --> T2[Remove undelivered leftovers]
    WP --> T3[Close stale connections]
    WP --> MON[Monitoring: job results]
    MON --> SCH
  • A job scheduler (such as Airflow or a cron job), daily or weekly, that tracks job status through monitoring.
  • A job queue for scheduled tasks, separate from the message queue.
  • A worker pool: executors in containers that pull jobs, ack them, and put them back on the queue if they fail.
  • Results go to monitoring, because this is not something you do once and forget; it has to improve over time.

The trade-off: scheduled jobs add operational complexity (another big thing to monitor and debug). The alternative is to let the garbage pile up until you are forced to do it anyway. So regular cleanup is essential for the system's long-term health.

ADVANCED

WhatsApp: failures, global deployment and feature flags

5 min read 180:35

The happy path (all services up and everyone happy) never happens in reality. There are failures in every part.

15.1Redis is down: fall back to the database 180:47

Redis holds the inboxes of undelivered messages. What if the whole cluster goes down?

  1. Writes: the server writes the undelivered message to the Redis inbox (the fastest), and also to the database as a durable backup.
  2. Reads: it tries Redis first. If it is empty or unavailable, it falls back to the DB.
  3. Recovery: when Redis comes back, a background job syncs it:
SELECT * FROM messages_inbox WHERE delivered IS NULL;
-- for each: write to Redis inbox, keep the DB record

Then we return to normal operation (writing and reading from Redis).

def get_undelivered_messages(user_id):
    try:
        messages = redis.get_inbox(user_id)           # fastest path
        if len(messages) > 0:
            return [parse(m) for m in messages]
    except RedisError:
        log("redis unavailable")                      # e.g. CloudWatch, for tracing
    return db.query("SELECT ... FROM inbox WHERE user_id = ? AND delivered IS NULL", user_id)

Slower, but better than the whole operation failing.

(Explainer's note): This code is my simplification of the logic the speaker explained (try Redis, log, fall back to a select from the DB), not a literal copy.

15.2Regions and global deployment 184:28

Users are everywhere in the world, so we use regions (such as us-east-1 and us-west-2 on AWS), and every region has a full stack: chat servers, Redis clusters and database shards, so it works on its own.

The speaker's example: user A in Tokyo opens the app:

  1. A DNS lookup connects them to the nearest IP, which is the Singapore region.
  2. The region's load balancer, then a chat server over the WebSocket.
  3. The number of chat servers varies by region (for example Ireland has many users so more servers, and Singapore fewer).
  4. The region's Redis knows where B is, so the message is sent to B's region.
  5. User B in London, same story: DNS, then the nearest IP, then the load balancer, and they receive the message from the region they are connected to.

15.3Kafka is down 186:34

Kafka itself is very scalable, but you have to plan for it going down. Inside the cluster there are brokers (servers), the topics are split into partitions, and each partition has a leader and replicas. Producers write to the leader, and the replicas are for redundancy.

Scenario What happens The fix
One broker goes down We replace it and add a new broker, and Kafka rebalances the partitions over time
The whole cluster goes down (rare but possible) The chat servers can't publish, and the storage service isn't consuming, so messages aren't stored: data loss A local buffer in the publish code
Kafka and the chat server go down together The in-memory buffer is gone An append-only log on local disk, replayed when the server comes back, then deleted after publishing

The publish logic with the buffer:

def publish_message(msg):
    try:
        kafka.publish(msg)                      # happy path
    except KafkaError as e:
        log(e)                                  # find out why later
        if len(local_buffer) >= 100_000:
            local_buffer.pop_oldest()           # drop oldest, never fail the whole operation
        local_buffer.append(msg)
        disk_log.append(msg)                    # survives a chat-server crash
        # retry interval must cover the time to spin up a new Kafka (e.g. ~2,000 ms)
  • The buffer is bounded (for example 100,000 messages). If it fills up, we don't fail the operation; we drop the oldest.
  • The retry interval must be longer than the time a new Kafka takes to come up. You need to know that number exactly (the speaker's example: a retry interval of about 2 seconds = 2,000 ms).

Recovery after Kafka is wiped completely:

  1. The message storage service requests messages after the last ID.
  2. It compares them with the messages actually stored in the DB.
  3. It republishes the missing ones to the new Kafka.
  4. Once everything is written to durable storage, it returns to normal consumption.

The goal: prevent, or at least reduce, data loss when many parts fail together.

(Explainer's note): This code is my simplification of the logic of the code shown on screen, not a literal copy. The numbers (100,000 and 2,000 ms) are the speaker's examples. The 2 seconds is the retry interval in his illustration, not a realistic time to bring up a whole new Kafka cluster, which in practice takes minutes.

15.4Feature flags and gradual rollout 191:34

In large systems you don't ship a new feature to all users at once; you do a gradual rollout with a feature flag store:

  • All the cloud providers have solutions (Azure, AWS), and there are external tools such as LaunchDarkly (the speaker says they used it at his previous company).
  • They are key-value pairs: for example is_new_theme_enabled = true/false, and the value differs by environment: true in staging and false in production. Developers see the feature, users don't.
  • In production: true for only 20% of users (beta), and if there are no failure logs we open it to everyone.
  • The chat server queries the flag store for the available features (per user or per group), sends messages with the new features to apps above a certain version (for example 2.2), and strips the unsupported fields for older apps.
BONUS

Bonus: how to actually get better

2 min read 193:39

The speaker's summary of the approach he followed across the three systems:

  1. Start with the requirements and the estimates: they tell you which storage you will use, and whether you need Kafka or not.
  2. Don't jump straight to Redis Pub/Sub or database sharding. Start with a simple architecture for thousands of users, and build it up naturally as you talk with the interviewer.
  3. Identify the bottlenecks: Kafka may go down, rate limiting, the database may go down... this is not something to memorize; know the steps and work through each system.
  4. Understand the trade-offs: every decision has a cost. WebSocket, for example, takes more memory than HTTP but gives real-time delivery, which is worth it in a messaging app. In another case, polling might be enough.
  5. Practice back-of-the-envelope calculations: work through examples and check your math.
  6. Failures after the design holds: don't rush to them before you have identified the core components and designed the whole system.
  7. There is no perfect design in an hour or two: they want to see how you think, how you discuss trade-offs, and whether you choose something just because you like it.
  8. Say your thinking out loud: if you are heading in the wrong direction, the interviewer will correct you and give you hints.

The speaker recommends the System design guide on his YouTube channel (the first pinned video) for all the concepts he used in the course.

REFERENCE

Glossary

3 min read
Term Short definition
Functional requirements What the system does: the core use cases, and what is out of scope
Non-functional requirements The system's constraints: number of users, read-to-write ratio, latency and availability
Back-of-the-envelope Quick rough estimates of scale (storage, QPS, bandwidth) before designing
QPS Queries per second: the number of queries (usually reads) per second
TPS Transactions per second: the number of writes or transactions per second
Availability The share of time the system is up, such as 99.9%
CAP theorem When there is a network partition, you have to choose between consistency and availability
Strong consistency Every read sees the latest write immediately
Eventual consistency Copies converge after a while, and in the meantime you may read stale data
Load balancer Distributes requests across multiple servers and runs health checks
API gateway A single entry point: authentication, rate limiting and routing to the services
CDN A network of servers close to users that caches content so delivery is faster
Blob storage Object storage for large files (audio, images, video), such as S3
Signed URL A temporary link with limited permissions to access a file in storage, which expires after a while
Pre-signed URL A signed URL the server generates so the client can upload or download a file directly from storage
Egress costs The cost of data leaving the cloud provider's network to reach users
JWT A signed token that proves the user's identity on every request
Stateless A server that keeps no session or data, so you can easily add more copies of it
LRU Least recently used: an eviction policy that removes whatever was requested least recently
TTL Time to live: how long an item in a cache, or a file, stays valid before it expires
Read replica A read-only copy of the database, kept in sync with the primary or leader
Sharding Splitting data across multiple database instances; improves writes and capacity
Shard key The field that decides which shard each row goes to, such as hotel_id or user_id
Modulo sharding hash(key) % N; simple, but adding a shard moves almost all the data
Consistent hashing A hash ring; adding a server moves only a small share of the keys
ACID Atomicity, Consistency, Isolation, Durability: the transaction guarantees in SQL
Read committed PostgreSQL's default isolation level: you see data only after commit, with a gap between the read and the write
Serializable The highest isolation level: transactions behave as if they ran one after another
SELECT FOR UPDATE Locks the rows you read until the end of the transaction, so anyone else who wants them waits
Race condition A wrong result because two operations happened at the same time, such as booking the last room twice
Idempotency key A unique ID for a request, so repeating it doesn't perform the operation twice, such as the reservation ID
Overbooking Allowing bookings beyond the inventory by a percentage (for example 10%) because cancellations are expected
Elasticsearch A search and analytics engine: full-text search, relevance ranking, typos and geographic filtering
CDC Change data capture: capturing database changes from the log and publishing them to other systems
Debezium An open-source CDC tool that reads the WAL and publishes to Kafka
Message queue A queue that decouples the sender from the processor, such as Kafka and RabbitMQ
Dead letter queue A separate queue for messages whose processing failed, so they can be handled later without being lost
WebSocket A persistent, bidirectional connection between the client and the server
Long polling The server holds the request until there is data or the timeout expires
Service discovery A registry where servers register themselves; it runs health checks, and the load balancer gets the list of healthy servers from it
Pub/Sub The publish/subscribe pattern: a publisher sends on a channel and the subscribers receive
Fanout Delivering one message to many recipients, such as group messages
Signal protocol An end-to-end encryption protocol using identity keys, prekeys and session keys
Token bucket A rate limiting algorithm: tokens refill at a fixed rate, and each request takes one
Feature flag A key-value that turns a feature on or off per environment or for a share of users
REFERENCE

Review questions

7 min read
Your scenario score:0 / 0
  1. A 60-minute interview, and the interviewer says "design Spotify". What is the first thing you do? Easy

  2. You chose PostgreSQL for a payments system because consistency matters most, and the choice is obvious. What do you do? Easy

  3. You finished the high-level design and 25 minutes are left. What is the best thing to do? Easy

  4. Explain the five dimensions you are evaluated on in a System Design interview, and why memorizing architectures is not enough. Easy

    Show answer

    Communication & collaboration, structured thinking, technical judgment, practical depth and time management. Memorization covers part of only one dimension, and the others show in how you run the conversation and handle decisions, failures and time. Section 0.3

  5. In Spotify, the app needs to play a 3 MB song. What does the API server do? Medium

  6. Spotify now has 50 million users, and the API servers can't keep up. What is the simplest step? Medium

  7. A user on Booking.com quickly clicks "Book" twice. What is the simplest fix the speaker preferred? Medium

  8. Booking.com search results must come back in under 500 ms, handle typos and geographic distance, and the load is about 1,000 QPS. What do you do? Medium

  9. A chat app must deliver messages in under 500 ms for millions of users. Which connection method? Medium

  10. Calculate Booking.com's TPS from 1.1 billion room nights a year and an average stay of two nights. Medium

    Show answer

    1.1 billion ÷ 2 = 550 million bookings/year. ÷ 365 = about 1.5 million/day. ÷ 86,400 = about 17 TPS. Section 7.4

  11. Why did the speaker put hotels and reservations in the same database even though he has separate services? Medium

    Show answer

    Pragmatic microservices: separation at the application level, but tightly related data in one DB for local ACID transactions instead of distributed transactions and their consistency problems. Section 8.3

  12. After a user books the last room, another user still sees it as available in search. Is this a bug that must be fixed immediately? Hard

  13. Two users book the last room at the same moment on PostgreSQL with default settings, and the TPS is high. The fix? Hard

  14. You have 16 shards with modulo hashing, and you expect to add shards every month. What do you do? Hard

  15. A user books and then immediately opens "My bookings", and the booking isn't shown because it reads from a replica. The fix? Hard

  16. A user in a 50-member group sends a message; 30 are online and 20 offline. The right design? Hard

  17. The message storage service writes 1,000 messages one at a time (5 ms each) and can't keep up. What do you do? Hard

  18. The whole Kafka cluster is down, and the chat servers are still receiving messages. What do you do so they aren't lost? Hard

  19. Compare sharding by user_id and by message_id in WhatsApp, and what solution did the speaker propose? Hard

    Show answer

    user_id: reads come from one shard, but there are hotspots. message_id: even distribution, but reads go across all the shards. The solution: user_id with a copy on the recipient's shard (double writes, fast reads) and replicas for reads. The decision depends on whether the system is optimized for reads or writes, so ask. Section 14

  20. If the Redis cluster goes down in WhatsApp, are the pending messages for offline users lost? Explain the design. Hard

    Show answer

    No. Every undelivered message is written to the Redis inbox and to the DB. Reads try Redis, and if that fails they fall back to the DB. When Redis comes back, a background job syncs the messages where delivered IS NULL. Section 15.1

One-line summary: clarify the requirements and run the numbers first, start with a simple design, choose every technology for a reason drawn from the requirements rather than habit, scale gradually (stateless servers, replicas, sharding, queues, caches), then design for failure, and think out loud with the interviewer at every step.

(Explainer's note): The diagrams in this guide are built from the speaker's words, but their layout and arrangement are mine. The chapter timestamps in the video description run ahead of (start before) the actual speech, by about 2–6 minutes in the later parts, so the ▶️ links here are taken from the subtitles themselves, not from the description.

LINKS · RESOURCES

Resources and links

Every link here comes from the video description.

References

More from the same channel

Follow the speaker

PRACTICE · REVIEW

Review flashcards

Answer in your head, then flip the card. Space flips; arrow keys move. "Again" brings the card back a little later.

The same cards are saved next to this guide as an Anki deck: cards.en.txt. To import it, open Anki and choose File, then Import.