← All sheets System Design

System Design Cheat Sheet (OOD + system design)

Version: 1. Date: 2026-11-14. Companion to the Software Engineering cheat sheet (OOP basics are there: A1, A2).

Main source for the method: Hello Interview "Low-Level Design in a Hurry" and "System Design in a Hurry" (read 2026-11-14). Their step names are kept; the rest is paraphrased. Where their example is behind a paywall, the design here is my own and says so.

Format (same as the other cheat sheet):

Word: simple explanation. Example: real example.


0. The words first

OOD (Object-Oriented Design): design the classes for a small system: which classes, what each one stores, what methods it has, how they connect. Also called LLD (Low-Level Design): the same interview, two names. Example: "Design a parking lot." You write classes like ParkingLot, ParkingSpot, Ticket, and some code.

System design (also HLD, High-Level Design): design a big system of services, databases, caches and queues, for many users. Boxes and arrows, usually no code. Example: "Design a URL shortener for 100 million users."

OOD / LLDSystem design / HLD
You designClasses, methods, how objects work togetherServices, databases, caches, queues
OutputClass list + code (or pseudocode)Box-and-arrow diagram
Scale (millions of users)?NoYes, it is the main point
Typical promptsParking lot, locker, library, elevator, vending machineURL shortener, chat app, news feed
Who gets itNew grads, mid-level, and as one round for seniorsMid-level, and always for senior (weighted heavily, see 7.1)

Pseudocode: code-like steps that do not need to compile. Example: if no free spot → reject.

"Leadership principle" questions: Amazon's name for behavioural (STAR) questions, based on their 16 Leadership Principles. Other companies have their own values. Same preparation: 6–8 real stories with numbers.


1. How to run an OOD interview (Hello Interview framework)

Total: about 35 minutes.

1. Requirements                  ~5 min
        ↓
2. Entities and Relationships    ~3 min
        ↓
3. Class Design                  ~10–15 min
        ↓
4. Implementation                ~10 min
        ↓
5. Extensibility                 ~5 min (if time)

Step 1. Requirements (~5 min)

Ask questions. Then write the agreed list, and a separate "Out of scope" list.

Ask about:

Scope: what is part of the task and what is not. Saying "out of scope" out loud shows control.

Step 2. Entities and Relationships (~3 min)

Entity: a "thing" in the problem that becomes a class.

How to find them: underline the nouns in the requirements.

State: the data an object holds right now. Example: spot = taken, by ticket T42. Orchestrator: the class that receives requests and coordinates the other classes. It is the "front door".

Step 3. Class Design (~10–15 min)

For each class, write what it stores (fields) and what it does (methods). Start from the orchestrator and work down.

ParkingSpot
  - id, size, ticket (or empty)
  + isFree(), assign(ticket), release()

(- = private field, + = public method. See section 3 for UML.)

Step 4. Implementation (~10 min)

Happy path: the normal case where nothing goes wrong. Edge case: an unusual case at the limits. Example: the last free spot; a ticket used twice.

Step 5. Extensibility (~5 min)

Extensibility: how easily the design takes a new requirement without a rewrite.

The interviewer adds something halfway ("now add electric car charging spots"). They want to see your design bend, not break.

Tips (from the advice you pasted, made simple)


2. The patterns that cover most OOD follow-ups

Keep this short. These 4 handle most "now add X" questions.

Interface for the part that varies: different classes, same method names. Example: PricingRule with calculate(hours).

Strategy pattern: put a rule that can change into its own class, and let the main class use whichever rule it is given. Swap the rule without editing the main class.

interface PricingStrategy { fee(hours: number): number; }           // the part that varies
class FlatRate   implements PricingStrategy { fee(h: number) { return h * 3; } }
class PeakRate   implements PricingStrategy { fee(h: number) { return h * 5; } }

class ParkingLot {
  constructor(private pricing: PricingStrategy) {}                    // given a rule, uses it
  exit(hours: number) { return this.pricing.fee(hours); }             // does not care which one
}

Enum: a fixed list of named options. Example: enum Size { SMALL, MEDIUM, LARGE }. Use it for things that do not have their own behaviour.

Singleton (be careful): only one object of a class can exist (example: one ParkingLot). Often asked about, but it makes testing harder. Say: "I would just create one, and pass it in where needed."


3. UML and diagram tools

What is UML?

UML (Unified Modeling Language): a standard set of diagram symbols for software design. It has many diagram types. In interviews you only need the class diagram (and sometimes the sequence diagram).

Class diagram: shows classes as boxes (name, fields, methods) and lines between them that show how they relate. Sequence diagram: shows one use case as messages between objects, top to bottom, in time order.

Is UML what people use for architecture diagrams? Mostly no:

Class diagram symbols (the ones that matter)

VISIBILITY

  +    public
  -    private
  #    protected


LINES BETWEEN CLASSES

  Child  ──────▷  Parent
         Inheritance ("is-a", extends)
         solid line, hollow triangle

  Class  - - - ▷  Interface
         Implements
         dashed line, hollow triangle

  Whole  ◆──────  Part
         Composition ("owns"; part dies with the whole)
         filled diamond

  Whole  ◇──────  Part
         Aggregation ("has"; part can live alone)
         hollow diamond

  A      ──────>  B
         Association ("uses / knows about")
         plain arrow


NUMBERS ON LINES

  1       exactly one
  0..1    none or one
  *       many

Composition vs aggregation: both mean "has-a". Composition = the part cannot exist without the whole (a ParkingSpot belongs to one lot). Aggregation = the part can exist alone (a Book exists without a Member).

Which tool?

ToolWhat it isWhen
Mermaid (used in this file)Diagrams written as text inside markdownNotes, docs, Git. Renders on GitHub, Obsidian, VS Code (with the "Markdown Preview Mermaid Support" extension), and at mermaid.live
draw.io (diagrams.net)Free drag-and-drop diagram app (web or desktop)Polished diagrams; can import Mermaid (Arrange → Insert → Advanced → Mermaid)
ExcalidrawFree hand-drawn-style whiteboardCommon in online system design interviews
Whiteboard / CoderPad textPlain text classesMost OOD interviews: simple notation is enough

How to view the diagrams below: open this file on GitHub, or paste a mermaid block into https://mermaid.live.


4. Example 1: Parking lot

Requirements are from Hello Interview's visible page. The class design and code are my own (their solution is paywalled).

Step 1. Requirements

Step 2. Entities

Nouns: lot, vehicle, spot, ticket, fee.

Step 3. Classes (simple notation)

enum SpotSize { SMALL, MEDIUM, LARGE }        // motorcycle, car, large vehicle

ParkingLot                                     // orchestrator
  - spots: ParkingSpot[]
  - activeTickets: Map<ticketId, Ticket>
  + enter(size): Ticket                        // find spot, make ticket
  + exit(ticketId): fee                        // check ticket, charge, free spot

ParkingSpot
  - id, size, ticket (or empty)
  + isFree(), fits(size), assign(ticket), release()

Ticket
  - id, spot, entryTime

Class diagram

classDiagram
  class ParkingLot {
    -ParkingSpot[] spots
    -Map~string,Ticket~ activeTickets
    +enter(size) Ticket
    +exit(ticketId) number
  }
  class ParkingSpot {
    -string id
    -SpotSize size
    -Ticket ticket
    +isFree() boolean
    +fits(size) boolean
    +assign(ticket)
    +release()
  }
  class Ticket {
    -string id
    -ParkingSpot spot
    -Date entryTime
  }
  class SpotSize {
    <<enumeration>>
    SMALL
    MEDIUM
    LARGE
  }
  ParkingLot "1" *-- "*" ParkingSpot : owns
  ParkingLot "1" --> "*" Ticket : tracks
  Ticket "1" --> "1" ParkingSpot : for
  ParkingSpot --> SpotSize

Step 4. Implementation (TypeScript, short)

enum SpotSize { SMALL, MEDIUM, LARGE }

class ParkingSpot {
  ticket: Ticket | null = null;
  constructor(readonly id: string, readonly size: SpotSize) {}
  isFree() { return this.ticket === null; }
  fits(size: SpotSize) { return this.size === size; }    // rule lives with the spot's data
}

class Ticket {
  readonly entryTime = Date.now();
  constructor(readonly id: string, readonly spot: ParkingSpot) {}
}

class ParkingLot {
  private active = new Map<string, Ticket>();
  constructor(private spots: ParkingSpot[], private ratePerHour = 3) {}

  enter(size: SpotSize): Ticket {
    const spot = this.spots.find(s => s.isFree() && s.fits(size));
    if (!spot) throw new Error("No free spot");           // edge case: full
    const ticket = new Ticket(crypto.randomUUID(), spot);
    spot.ticket = ticket;
    this.active.set(ticket.id, ticket);
    return ticket;
  }

  exit(ticketId: string): number {
    const ticket = this.active.get(ticketId);
    if (!ticket) throw new Error("Invalid or used ticket"); // edge case: bad ticket
    const hours = Math.ceil((Date.now() - ticket.entryTime) / 3_600_000); // round up
    ticket.spot.ticket = null;                               // free the spot
    this.active.delete(ticketId);                            // ticket cannot be used again
    return hours * this.ratePerHour;
  }
}

Walk one use case through it (say this out loud):

CAR ARRIVES
  enter(MEDIUM)
    → find free MEDIUM spot S2
    → new ticket T1
    → S2.ticket = T1
    → T1 saved

2.5 HOURS LATER
  exit(T1)
    → T1 found
    → hours = 3 (rounded up)
    → S2 freed
    → T1 removed
    → fee = 9

SAME TICKET AGAIN
  exit(T1)
    → not found
    → "Invalid or used ticket"

Step 5. Extensibility (likely follow-ups)

Follow-upWhere the change goes
Different price per vehicle typeMove the fee into a PricingStrategy interface (section 2). ParkingLot gets the strategy in its constructor. No other class changes
Multi-floor garageAdd a Level class that owns its spots. ParkingLot owns levels and asks each level for a free spot
Small car may use a bigger spotChange only ParkingSpot.fits() (e.g. this.size >= size)
Multiple entrances at the same timeTwo cars could get the same spot (a race condition). Make "find and assign a spot" one locked step (a mutex, or a database transaction)

5. Example 2: Locker system (e.g. Amazon Locker)

Requirements and classes are from Hello Interview's Amazon Locker page (summarised). The follow-ups at the end come from a summary, so treat them as likely, not confirmed.

Step 1. Requirements

Step 2. Entities

Step 3. Classes (simple notation)

enum Size { SMALL, MEDIUM, LARGE }

Locker                                         // orchestrator
  - compartments: Compartment[]
  - tokens: Map<code, AccessToken>
  + depositPackage(size): code
  + pickup(code)
  + openExpiredCompartments()

Compartment
  - size, occupied
  + isOccupied(), markOccupied(), markFree(), open()

AccessToken
  - code, expiration, compartment
  + isExpired()

Class diagram

classDiagram
  class Locker {
    -Compartment[] compartments
    -Map~string,AccessToken~ tokens
    +depositPackage(size) string
    +pickup(code)
    +openExpiredCompartments()
  }
  class Compartment {
    -Size size
    -boolean occupied
    +isOccupied() boolean
    +markOccupied()
    +markFree()
    +open()
  }
  class AccessToken {
    -string code
    -Date expiration
    -Compartment compartment
    +isExpired() boolean
  }
  class Size {
    <<enumeration>>
    SMALL
    MEDIUM
    LARGE
  }
  Locker "1" *-- "*" Compartment : owns
  Locker "1" --> "*" AccessToken : maps code to
  AccessToken "1" --> "1" Compartment : opens
  Compartment --> Size

Step 4. Implementation (the 2 main methods)

depositPackage(size: Size): string {
  const c = this.compartments.find(c => c.size === size && !c.isOccupied());
  if (!c) throw new Error("No free compartment");     // edge case: full
  c.open();
  c.markOccupied();
  const token = new AccessToken(randomCode(), addDays(new Date(), 7), c);
  this.tokens.set(token.code, token);
  return token.code;                                   // driver gives this to the customer
}

pickup(code: string): void {
  const token = this.tokens.get(code);
  if (!token) throw new Error("Invalid code");
  if (token.isExpired()) throw new Error("Code expired");
  token.compartment.open();
  token.compartment.markFree();
  this.tokens.delete(code);                            // code cannot be used again
}

Walk-through:

DRIVER
  depositPackage(MEDIUM)
    → compartment M3
    → code 48213 given to the driver

CUSTOMER
  pickup("48213")
    → M3 opens and is freed
    → code deleted

Step 5. Extensibility (likely follow-ups)

Follow-upWhere the change goes
No free medium → use a large oneOnly the "find a compartment" line in depositPackage (look at the same size, then bigger sizes)
A broken compartmentReplace occupied: boolean with a status enum: AVAILABLE, OCCUPIED, OUT_OF_SERVICE
Driver reserves, then confirms or cancelsAdd a RESERVED status with a timeout; free it if not confirmed in time
Many locker sitesA LockerNetwork class that owns many Lockers and picks one near the customer

6. Example 3: Library checkout

Hello Interview has no library page (checked 2026-11-14), so this design is my own, made with the same 5 steps.

Step 1. Requirements

Step 2. Entities

Step 3. Classes (simple notation)

Library                                         // orchestrator
  - books: Map<isbn, Book>
  - members: Map<id, Member>
  + borrow(memberId, isbn): Loan
  + returnCopy(copyId): fine
  + search(title): Book[]

Book
  - isbn, title, copies: BookCopy[]
  + availableCopy(): BookCopy | null

BookCopy
  - id, book, onLoan
  + isAvailable()

Member
  - id, loans: Loan[]
  + canBorrow()                                 // rule: max 3

Loan
  - copy, member, dueDate, returnedAt
  + daysLate()

Class diagram

classDiagram
  class Library {
    -Map~string,Book~ books
    -Map~string,Member~ members
    +borrow(memberId, isbn) Loan
    +returnCopy(copyId) number
    +search(title) Book[]
  }
  class Book {
    -string isbn
    -string title
    +availableCopy() BookCopy
  }
  class BookCopy {
    -string id
    -boolean onLoan
    +isAvailable() boolean
  }
  class Member {
    -string id
    -Loan[] loans
    +canBorrow() boolean
  }
  class Loan {
    -Date dueDate
    -Date returnedAt
    +daysLate() number
  }
  Library "1" o-- "*" Book : catalogue
  Library "1" o-- "*" Member
  Book "1" *-- "*" BookCopy : copies
  Member "1" --> "*" Loan : has
  Loan "1" --> "1" BookCopy : of

Step 4. Implementation (borrow)

borrow(memberId: string, isbn: string): Loan {
  const member = this.members.get(memberId);
  const book = this.books.get(isbn);
  if (!member || !book) throw new Error("Unknown member or book");
  if (!member.canBorrow()) throw new Error("Loan limit reached");   // rule lives in Member
  const copy = book.availableCopy();
  if (!copy) throw new Error("No copy available");
  copy.onLoan = true;
  const loan = new Loan(copy, member, addDays(new Date(), 14));
  member.loans.push(loan);
  return loan;
}

Walk-through:

MEMBER M1 (has 2 loans) BORROWS "Dune"
  → canBorrow() = true
  → copy C2 is free
  → C2 on loan
  → new loan, due in 14 days
  → M1 now has 3 loans

M1 BORROWS AGAIN
  → canBorrow() = false
  → "Loan limit reached"

Step 5. Extensibility (likely follow-ups)

Follow-upWhere the change goes
Reserve a book when all copies are outA Reservation queue per Book; on return, the copy goes to the first person in the queue
Different fine rules (students pay less)A FinePolicy strategy (section 2) given to Library or chosen per member type
Different loan limits per member typeMember gets a maxLoans field, or StudentMember / StaffMember subclasses if their behaviour really differs
Many branchesA Branch class owns copies; Library owns branches

7. System design (HLD)

7.1 What it is, and why it matters more as you get senior

System design (HLD, High-Level Design): design a whole product that many people use at once: which services (programs that do one job), which databases, where to add caches and queues, and how they talk to each other. You draw boxes and arrows and explain your choices. Usually no code. Example: "Design Ticketmaster." You do not write a Ticket class. You decide: one service for search, one for booking, which database stops two people buying the same seat, and how to survive 10 million fans at 9 a.m.

What the interviewer really checks (Hello Interview's themes, made simple):

There is no single right answer. Two different designs can both pass. The reasons you give are what scores.

Who gets it, and what changes by level (Hello Interview):

LevelHow commonWhat they expect
Entry / new gradUsually no system design round (OOD instead)—
Mid-levelCommonA working design that covers the basics. About 80% breadth, 20% depth: you can name a component even if you only know it at the surface. The interviewer often leads the deep dives
SeniorNormal, and weighted heavilyGo through the basics fast, then spend most of the time on depth. You find the weak points and start the deep dives yourself, without waiting to be asked
Staff+NormalEven more depth and judgement (Hello Interview covers this separately)

For a senior role: expect HLD rounds, and expect them to judge you at senior level: move quickly through requirements and the basic design, then say "the hard parts here are X and Y; let me go deeper on X."

7.2 The framework (Hello Interview), with tips and mistakes

1. Requirements                  ~5 min
        ↓
2. Core Entities                 ~2 min
        ↓
3. API or System Interface       ~5 min
        ↓
4. [Optional] Data Flow          ~5 min
        ↓
5. High Level Design             ~10–15 min
        ↓
6. Deep Dives                    ~10 min
StepWhat you doTipsCommon mistake
RequirementsFunctional = what it does ("users can book a ticket"). Non-functional = how well (speed, scale, never lose data)Pick the top 3 functional ones; put the rest "below the line" (out of scope). Give the non-functional ones numbers ("redirect < 100 ms")A long list that eats the time
Back-of-envelope mathsRough sums (users × requests, storage size)Do it only if the answer changes the design, and say soSpending 10 minutes on maths that changes nothing
Core EntitiesList the main data "things" (User, Event, Ticket)A quick first list; you can change it laterWriting the full database tables now
APIThe endpoints: POST /bookings, GET /events/:idDefault to REST. Use WebSocket/SSE for real-time. Take the user's ID from the login token, not from the request bodyTrusting the client for the user ID or the price (they can fake it)
Data Flow (optional)Numbered steps data goes throughOnly for pipeline systems (e.g. a web crawler)Doing it for simple apps
High Level DesignBoxes and arrows. Go one endpoint at a time until every functional requirement worksKeep it simple first; just mention caches/queues and move onAdding complex parts too early and never finishing
Deep DivesFix the non-functional requirements: scale, speed, failures, edge casesSenior: you start them. Let the interviewer steer too; do not talk over themNo working end-to-end design (the biggest failure)

Below the line: the requirements you agree not to build in this interview. Saying them out loud shows you thought of them.

7.3 Building blocks (the boxes you draw)

Some of these are also in the software cheat sheet (A14). Short versions here:

BlockWhat it isUse it for
Load balancerSpreads requests over many copies of a serviceEvery service with real traffic
API gatewayOne front door: routes requests, checks login, rate limitsMany services behind one URL
Cache (Redis, Memcached)Fast in-memory store for data read oftenHot reads: short URL → long URL
CDNServers around the world that keep copies of filesImages, videos, static pages
SQL database (PostgreSQL)Tables, joins, transactions (all-or-nothing)Money, bookings: anything that must be exactly right
NoSQL database (DynamoDB, MongoDB)Flexible records, easy to spread over many machinesHuge scale, simple lookups by key
Blob storage (S3)Stores big files cheaplyPhotos, videos, attachments
Message queue (SQS, Kafka)Jobs wait in a list; workers process them laterSlow work, traffic spikes
Pub/Sub (Redis Pub/Sub, Kafka)Senders "publish" to a channel; every listener gets itLive messages to many users
Search index (Elasticsearch)A database built for fast text search"Find events with 'Taylor' in the name"
WebSocket / SSEA connection that stays open so the server can push updatesChat, live seats, driver location

Words that come up in deep dives:

Read-heavy / write-heavy: many more reads than writes (a URL is created once, clicked 1,000 times), or the opposite. Read-heavy → add caches and read copies. Replication: keep copies of the database on several machines. More reads possible; survives a machine failure. Sharding (partitioning): split the data over several databases (e.g. users A–M on one, N–Z on another), because one machine is too small. Consistent hashing: a way to decide which shard holds which data, so adding a new server moves only a small part of the data. Hot key: one item that gets a huge share of traffic (a celebrity's post). It can overload the one server that holds it. Distributed lock: a lock that works across many servers, often in Redis. Example: "seat 14B is held by user 7 for 10 minutes." TTL (Time To Live): data that deletes itself after a set time. Example: the seat hold above. Fan-out: one action that must reach many places. Example: one post must appear in 1 million followers' feeds. CDC (Change Data Capture): watch the database for changes and copy them somewhere else (e.g. into the search index). Availability vs consistency: when part of the system fails, do you keep answering (maybe with old data) or refuse until the data is correct? Bookings and payments → consistency. Feeds and search → availability.

Resilience words (how the system keeps working when a part fails):

TermSimple meaningExample
TimeoutStop waiting after N secondsBank call: 5 s
Retry + backoffTry again, waiting longer (1 s, 2 s, 4 s)Gateway hiccup. Only safe with idempotency
Circuit breakerAfter many failures, stop calling a broken service for a while, then test it againBank down → fail fast, show "try later" (no waiting on timeouts)
Dead-letter queueA separate place for jobs that keep failing, so they stop blocking the main queue; a person checks them laterOne bad email address does not block the others
Rate limitingMax requests per user5 login attempts per minute
Horizontal / vertical scalingAdd more servers / make one server bigger (more CPU, RAM)More pods (copies of the service) in Kubernetes
CAP theoremWhen the network between servers breaks, you must choose: consistency (everyone sees the same, correct data; some requests fail) or availability (always answer, maybe with old data)Payments → consistency
Fail-safeGo to a safe state on a faultStop processing rather than send wrong data
Graceful degradationWhen one part fails, keep the core features working with less functionPayments work; loyalty points update later

7.4 The most common questions

From Hello Interview's problem list (rated Easy / Medium / Hard) and prep-site roundups. I could not open Reddit threads directly, so the "common" ranking comes from prep sites that summarise those discussions.

ProblemHello Interview levelThe one key idea it tests
URL shortener (Bitly)EasyUnique short codes; very fast reads with a cache
Dropbox / Google DriveEasy / HardUpload big files straight to blob storage; sync changes
TicketmasterMediumNever sell one seat twice (locks, consistency) under a huge rush
News feed (Facebook / Twitter)MediumFan-out: build feeds ahead of time vs on read; celebrities
WhatsApp / chatMediumReal-time delivery to online users, and storing for offline users
Rate limiterMediumCount requests fast across many servers (token bucket in Redis)
YouTubeMediumVideo upload, processing and streaming via CDN
Notification systemMediumQueues, retries, many channels (email, SMS, push). Your link: your notifications service
UberHardMillions of live driver locations; match each ride to one driver
Web crawlerHardA big pipeline with queues; politeness; no duplicate pages
Payment system (Stripe)HardIdempotency, never double charge, exact records

The 5 in bold are worked through below. Do these first.

7.5 Worked examples (same steps every time)

Each example follows: Requirements → Entities → API → Design (diagram) → Deep dives. Requirements and chosen solutions follow Hello Interview's breakdowns, simplified.

Example A: URL shortener (Bitly), easy

Requirements

Entities: Url (short code, long URL, expiry), User.

API

POST /urls
     body:    { longUrl, customAlias?, expiresAt? }
     returns: { shortUrl }

GET  /{code}
     returns: 302 redirect to the long URL
              (410 Gone if expired)

302 redirect: an HTTP answer that says "go to this other address". The browser follows it at once.

Design

flowchart LR
  C[Client] --> LB[Load balancer]
  LB --> W[Write service]
  LB --> R[Read / redirect service]
  W --> CNT[(Redis counter)]
  W --> DB[(Database)]
  R --> CACHE[(Cache)]
  CACHE -. miss .-> DB

Deep dives

ProblemChosen answer (simple)
How to make codes unique and short?A counter in Redis gives the next number (1, 2, 3…); turn it into letters and digits with base62 (a–z, A–Z, 0–9). 7 characters ≈ 3.5 trillion codes. Each write server takes a batch of numbers at once, so it does not ask Redis every time. A unique rule in the database is the final safety check
How to make redirects < 100 ms?Put short → long in a cache. Most clicks never reach the database. A CDN can help further
How big is the data?~500 bytes per row × 1 billion ≈ 500 GB. One good database can hold this; split read and write services so each can scale on its own

Base62: write a number using 62 symbols instead of 10, so it gets shorter. Example: 1,000,000 → 4c92.

Example B: Ticketmaster, medium

Requirements

Entities: Event, Venue, Performer, Ticket (a seat), Booking, User.

API

GET  /events/:eventId
     returns: the event

GET  /events/search?keyword=&start=&end=&page=
     returns: matching events

POST /bookings/:eventId
     body:    { ticketIds, paymentDetails }
     returns: { bookingId }

Design

flowchart LR
  C[Client] --> GW[API gateway]
  GW --> ES[Event service]
  GW --> SS[Search service]
  GW --> BS[Booking service]
  ES --> DB[(PostgreSQL)]
  BS --> DB
  BS --> LOCK[(Redis: seat holds)]
  BS --> PAY[Payment provider]
  SS --> IDX[(Elasticsearch)]
  DB -. CDC .-> IDX

Deep dives

ProblemChosen answer (simple)
Two people pick the same seatWhen you choose a seat, put a hold on it in Redis with a 10-minute TTL. Others see it as taken. If you do not pay in time, the hold expires by itself. When the payment provider confirms, mark the ticket sold in PostgreSQL (in a transaction)
Why not lock the row in the database for 10 minutes?Long database locks block other work and do not scale. A short-lived Redis hold is lighter
10 million fans at 9 a.m.A virtual waiting room: put users in a queue and let them in in batches; tell them their place over WebSocket/SSE
Fast searchCopy events into Elasticsearch (kept in sync with CDC); cache popular searches

Example C: News feed (Facebook / Twitter), medium

Requirements

Entities: User, Follow (who follows whom), Post.

API

POST /posts
     body:    { content }
     returns: { postId }

PUT  /users/:id/follow

GET  /feed?pageSize=&cursor=
     returns: { items, nextCursor }

(Cursor = a bookmark for "the next page after this post"; see the software cheat sheet A6.)

Design

flowchart LR
  C[Client] --> GW[API gateway]
  GW --> PS[Post service]
  GW --> FS[Feed service]
  PS --> POSTS[(Posts table)]
  PS --> Q[Queue]
  Q --> WK[Fan-out workers]
  WK --> FEED[(Precomputed feeds)]
  FS --> FEED
  FS --> POSTS
  FS --> CACHE[(Post cache)]

Deep dives

ProblemChosen answer (simple)
Building a feed on read is slow (you follow 1,000 people)Fan-out on write: when someone posts, workers add the post ID to each follower's precomputed feed (~200 IDs per user). Reading the feed is then one fast lookup
A celebrity with 100 million followers postsWriting to 100 million feeds is too much. Hybrid: do not fan out for huge accounts; merge their recent posts in at read time
One post goes viral (hot key)Keep popular posts in a cache with copies on many cache servers, so no single server takes all the reads

Fan-out on write vs on read: do the work when the post is written (fast reads, heavy writes) or when the feed is read (simple writes, slow reads).

Example D: WhatsApp / chat, medium

Requirements

Entities: User, Chat, Message, Client (one per device), Inbox (messages waiting for a user).

API (over a WebSocket, because the server must push messages)

CLIENT → SERVER
  sendMessage(chatId, text)
  createChat(...)
  ack(messageId)

SERVER → CLIENT
  newMessage(...)
  chatUpdate(...)

Ack (acknowledgement): the phone tells the server "I got message X", so the server can stop trying to deliver it.

Design

flowchart LR
  A[Phone A] <-->|WebSocket| CS1[Chat server 1]
  B[Phone B] <-->|WebSocket| CS2[Chat server 2]
  CS1 --> DB[(Messages + Inbox, with TTL)]
  CS1 --> PUB[(Pub/Sub)]
  PUB --> CS2
  A --> S3[(Blob storage: images)]

Deep dives

ProblemChosen answer (simple)
A and B are connected to different chat serversEach chat server subscribes to a Pub/Sub channel for each user connected to it. To send to B, publish to "B's channel"; B's server receives it and pushes it down B's WebSocket
Never lose a messageSave to the database (message + inbox) first, then publish. If the push fails, the message is still in B's inbox; it is sent when B comes back online and deleted after B's ack
Big filesThe phone uploads straight to blob storage using a short-lived upload link; the message only carries the link
Is a phone still connected?Heartbeats: small "still here" messages every few seconds

Example E: Rate limiter, medium

Requirements

Interface

isRequestAllowed(clientId, ruleId)
     returns: { allowed, remaining, resetTime }

Design

flowchart LR
  C[Client] --> GW[API gateway + rate limiter]
  GW -->|allowed| API[Backend services]
  GW -->|429 Too Many Requests| C
  GW <--> R[(Redis: token buckets)]

Deep dives

ProblemChosen answer (simple)
Where does the limiter live?In the API gateway: every request passes it once. (Inside each server = each server counts alone, so limits are wrong. A separate service = an extra network call)
Which counting method?Token bucket: each client has a bucket of tokens that refills at a steady rate; each request takes one; empty bucket → 429. Allows short bursts, simple to store
Many gateways count the same clientStore the buckets in Redis, shared by all gateways. Do "read, check, update" as one step (a Lua script in Redis), so two requests cannot both take the last token
Redis goes downDecide on purpose: fail open (allow everything; risky) or fail closed (block; safer for security). Hello Interview chooses fail closed

Token bucket vs fixed window: fixed window = "100 per clock minute" (simple, but allows 200 across the minute boundary). Token bucket = smooth refill, no boundary problem.

7.6 Your own system as an HLD answer

You can describe your notifications system in the same 5 steps:

flowchart LR
  UI[React UI] --> API[Notifications service: Express API]
  API --> MS[Other microservices: get data]
  API --> T[Fill template per type]
  T --> Q[(Agenda queue in MongoDB)]
  Q --> P[Agenda processor: one job at a time]
  P --> E[Email service]

8. Quick checklist for the interview

System design (HLD):

If they dig deeper

They askYou say
"Why is this a class and not a field?""It holds state that changes and has its own rules. A field would push those rules into another class."
"Why did you put this method here?""It works on this class's data, so this class should own it." (Information Expert)
"How would you test this?""Unit tests per class, with the orchestrator tested through its public methods: happy path, full lot, bad ticket."
"What if two requests come at the same time?""Finding and taking a spot must be one step, with a lock or a database transaction, so two cars cannot get the same spot."
"Would you use a Singleton for the lot?""I would create one and pass it in. A Singleton is hidden global state and makes tests harder."

Sources (read 2026-11-14):