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 / LLD | System design / HLD | |
|---|---|---|
| You design | Classes, methods, how objects work together | Services, databases, caches, queues |
| Output | Class list + code (or pseudocode) | Box-and-arrow diagram |
| Scale (millions of users)? | No | Yes, it is the main point |
| Typical prompts | Parking lot, locker, library, elevator, vending machine | URL shortener, chat app, news feed |
| Who gets it | New grads, mid-level, and as one round for seniors | Mid-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:
- What it must do (main actions). Example: "park a car, pay on exit".
- Rules. Example: "can a motorcycle use a car spot?"
- Errors. Example: "what if the lot is full? what if a ticket is used twice?"
- Limits of scope. Example: "is payment in scope, or just the fee?"
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.
- Make it a class if it holds state that changes or enforces rules. Example: a
ParkingSpotchanges from free to taken. - Make it a field (just a value) if it does not. Example: a vehicle's size can be a field; it does not need a
Vehicleclass unless it has behaviour. - Find the orchestrator: the one main class the outside world talks to. Example:
ParkingLot.
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.
- Keep each rule with the class that owns the data. Example: "is this spot free?" lives in
ParkingSpot, not inParkingLot. (Hello Interview calls this Information Expert: the class with the information does the work.) - Hello Interview advises simple class notation, not full UML, in the interview. Example of simple notation:
ParkingSpot
- id, size, ticket (or empty)
+ isFree(), assign(ticket), release()
(- = private field, + = public method. See section 3 for UML.)
Step 4. Implementation (~10 min)
- Ask first: "Do you want real code or pseudocode?"
- Write the happy path first (everything works), then the edge cases (full lot, bad ticket).
- Then walk one real use case through your code, step by step. This is where you find your own bugs.
- Use a design pattern only if it helps. Too many patterns = over-engineering, a common mistake.
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.
- Show where the change goes (which class, which method).
- If one part has many versions (pricing rules, vehicle types), put it behind an interface or a strategy class (see section 2).
- Junior: maybe one follow-up. Mid-level: one or two. Senior: several.
Tips (from the advice you pasted, made simple)
- Use a language you write fast (for you: TypeScript). They often want "runnable-ish" code, not only boxes.
- Say why you split things the way you did. Example: "I put the fee rule in its own class because it is the part most likely to change."
- Practise 2–3 problems on a timer, with someone asking "what if…?" every 10 minutes. The pressure is the part reading does not train.
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:
PricingRulewithcalculate(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:
- OOD → class diagrams (UML or simpler).
- System design → informal box-and-arrow diagrams (service → database). Not strict UML. Some teams use the C4 model (diagrams at 4 zoom levels: context, containers, components, code).
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
ParkingSpotbelongs to one lot). Aggregation = the part can exist alone (aBookexists without aMember).
Which tool?
| Tool | What it is | When |
|---|---|---|
| Mermaid (used in this file) | Diagrams written as text inside markdown | Notes, 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) |
| Excalidraw | Free hand-drawn-style whiteboard | Common in online system design interviews |
| Whiteboard / CoderPad text | Plain text classes | Most 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
- 3 vehicle types: motorcycle, car, large vehicle.
- On entry: give a compatible free spot and a ticket with a unique ID. If no spot fits → reject.
- On exit: check the ticket, charge per hour (round up), free the spot. Invalid or used ticket → reject.
- Same hourly rate for all vehicles.
- Out of scope: payment processing, gate hardware, cameras, UI, reservations.
Step 2. Entities
Nouns: lot, vehicle, spot, ticket, fee.
ParkingLot→ orchestrator (entry and exit go through it).ParkingSpot→ class (changes free ↔ taken).Ticket→ class (holds entry time; becomes "used").- Vehicle → just a size value (an enum). No behaviour, so no class.
- Fee → a method for now; may become a strategy later.
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-up | Where the change goes |
|---|---|
| Different price per vehicle type | Move the fee into a PricingStrategy interface (section 2). ParkingLot gets the strategy in its constructor. No other class changes |
| Multi-floor garage | Add a Level class that owns its spots. ParkingLot owns levels and asks each level for a free spot |
| Small car may use a bigger spot | Change only ParkingSpot.fits() (e.g. this.size >= size) |
| Multiple entrances at the same time | Two 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
- A driver deposits a package of size small, medium or large. The system gives a free compartment of that size, or rejects if none is free.
- Each deposit returns one access code (token).
- The customer picks up with the code. Wrong or expired code → a clear error.
- Codes expire after 7 days. The package stays until staff remove it.
- Staff can open all expired compartments.
- Out of scope: delivery logistics, sending the code to the customer, lock-out after failed tries, UI, many locker sites, payment.
Step 2. Entities
Locker→ orchestrator (deposit, pickup, staff action).Compartment→ class (free ↔ occupied).AccessToken→ class (code, expiry, which compartment; can expire).- Size → enum. Package is not a class: its size is just an input value. (Good point to say out loud: not every noun is a class.)
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-up | Where the change goes |
|---|---|
| No free medium → use a large one | Only the "find a compartment" line in depositPackage (look at the same size, then bigger sizes) |
| A broken compartment | Replace occupied: boolean with a status enum: AVAILABLE, OCCUPIED, OUT_OF_SERVICE |
| Driver reserves, then confirms or cancels | Add a RESERVED status with a timeout; free it if not confirmed in time |
| Many locker sites | A 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
- Members can borrow a book copy if one is available. Maximum 3 loans per member.
- Members return a copy. A late return gives a fine (e.g. 20p per day).
- Search the catalogue by title.
- Out of scope: payment of fines, accounts and login, UI, e-books.
Step 2. Entities
Library→ orchestrator.Book(the title, e.g. "Dune") vsBookCopy(one physical copy on the shelf). Say this split out loud: one book can have 5 copies; you lend a copy, not the title.Member→ class (has current loans; enforces "max 3").Loan→ class (which copy, which member, due date, returned or not).
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-up | Where the change goes |
|---|---|
| Reserve a book when all copies are out | A 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 type | Member gets a maxLoans field, or StudentMember / StaffMember subclasses if their behaviour really differs |
| Many branches | A 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
Ticketclass. 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):
- Problem navigation: can you turn a vague prompt into a clear list of what to build?
- Solution design: does your design actually work, end to end?
- Technical excellence: do you know the building blocks and their trade-offs (what you gain, what you give up)?
- Communication: do you explain your thinking and work with the interviewer?
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):
| Level | How common | What they expect |
|---|---|---|
| Entry / new grad | Usually no system design round (OOD instead) | — |
| Mid-level | Common | A 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 |
| Senior | Normal, and weighted heavily | Go 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+ | Normal | Even 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
| Step | What you do | Tips | Common mistake |
|---|---|---|---|
| Requirements | Functional = 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 maths | Rough sums (users × requests, storage size) | Do it only if the answer changes the design, and say so | Spending 10 minutes on maths that changes nothing |
| Core Entities | List the main data "things" (User, Event, Ticket) | A quick first list; you can change it later | Writing the full database tables now |
| API | The endpoints: POST /bookings, GET /events/:id | Default to REST. Use WebSocket/SSE for real-time. Take the user's ID from the login token, not from the request body | Trusting the client for the user ID or the price (they can fake it) |
| Data Flow (optional) | Numbered steps data goes through | Only for pipeline systems (e.g. a web crawler) | Doing it for simple apps |
| High Level Design | Boxes and arrows. Go one endpoint at a time until every functional requirement works | Keep it simple first; just mention caches/queues and move on | Adding complex parts too early and never finishing |
| Deep Dives | Fix the non-functional requirements: scale, speed, failures, edge cases | Senior: you start them. Let the interviewer steer too; do not talk over them | No 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:
| Block | What it is | Use it for |
|---|---|---|
| Load balancer | Spreads requests over many copies of a service | Every service with real traffic |
| API gateway | One front door: routes requests, checks login, rate limits | Many services behind one URL |
| Cache (Redis, Memcached) | Fast in-memory store for data read often | Hot reads: short URL → long URL |
| CDN | Servers around the world that keep copies of files | Images, 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 machines | Huge scale, simple lookups by key |
| Blob storage (S3) | Stores big files cheaply | Photos, videos, attachments |
| Message queue (SQS, Kafka) | Jobs wait in a list; workers process them later | Slow work, traffic spikes |
| Pub/Sub (Redis Pub/Sub, Kafka) | Senders "publish" to a channel; every listener gets it | Live messages to many users |
| Search index (Elasticsearch) | A database built for fast text search | "Find events with 'Taylor' in the name" |
| WebSocket / SSE | A connection that stays open so the server can push updates | Chat, 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):
| Term | Simple meaning | Example |
|---|---|---|
| Timeout | Stop waiting after N seconds | Bank call: 5 s |
| Retry + backoff | Try again, waiting longer (1 s, 2 s, 4 s) | Gateway hiccup. Only safe with idempotency |
| Circuit breaker | After many failures, stop calling a broken service for a while, then test it again | Bank down → fail fast, show "try later" (no waiting on timeouts) |
| Dead-letter queue | A separate place for jobs that keep failing, so they stop blocking the main queue; a person checks them later | One bad email address does not block the others |
| Rate limiting | Max requests per user | 5 login attempts per minute |
| Horizontal / vertical scaling | Add more servers / make one server bigger (more CPU, RAM) | More pods (copies of the service) in Kubernetes |
| CAP theorem | When 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-safe | Go to a safe state on a fault | Stop processing rather than send wrong data |
| Graceful degradation | When one part fails, keep the core features working with less function | Payments 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.
| Problem | Hello Interview level | The one key idea it tests |
|---|---|---|
| URL shortener (Bitly) | Easy | Unique short codes; very fast reads with a cache |
| Dropbox / Google Drive | Easy / Hard | Upload big files straight to blob storage; sync changes |
| Ticketmaster | Medium | Never sell one seat twice (locks, consistency) under a huge rush |
| News feed (Facebook / Twitter) | Medium | Fan-out: build feeds ahead of time vs on read; celebrities |
| WhatsApp / chat | Medium | Real-time delivery to online users, and storing for offline users |
| Rate limiter | Medium | Count requests fast across many servers (token bucket in Redis) |
| YouTube | Medium | Video upload, processing and streaming via CDN |
| Notification system | Medium | Queues, retries, many channels (email, SMS, push). Your link: your notifications service |
| Uber | Hard | Millions of live driver locations; match each ride to one driver |
| Web crawler | Hard | A big pipeline with queues; politeness; no duplicate pages |
| Payment system (Stripe) | Hard | Idempotency, 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
- Functional: (1) create a short URL from a long one (optional custom alias, expiry); (2) visiting the short URL redirects to the long one.
- Non-functional: codes are unique; redirect < 100 ms; very high availability; 1 billion URLs, 100 million daily users; read-heavy.
- Below the line: user accounts, click analytics.
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
| Problem | Chosen 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
- Functional: (1) view an event; (2) search events; (3) book tickets.
- Non-functional: never double-book a seat (consistency for booking); search and view always up (availability); handle ~10 million users for one big event; search < 500 ms; read-heavy (100 reads per booking).
- Below the line: viewing your bookings, admin tools, dynamic pricing.
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
| Problem | Chosen answer (simple) |
|---|---|
| Two people pick the same seat | When 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 search | Copy events into Elasticsearch (kept in sync with CDC); cache popular searches |
Example C: News feed (Facebook / Twitter), medium
Requirements
- Functional: (1) create a post; (2) follow people; (3) see a feed of followed people's posts, newest first; (4) scroll (page) through it.
- Non-functional: highly available (a feed up to 1 minute old is OK); post and feed < 500 ms; 2 billion users; no limit on follows or followers.
- Below the line: likes, comments, private posts.
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
| Problem | Chosen 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 posts | Writing 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
- Functional: (1) group chats (up to 100 people); (2) send and receive messages; (3) users who are offline get messages later (kept up to 30 days); (4) send pictures/files.
- Non-functional: delivery to online users < 500 ms; never lose a message; billions of users; keep messages only as long as needed.
- Below the line: voice/video calls, sign-up.
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
| Problem | Chosen answer (simple) |
|---|---|
| A and B are connected to different chat servers | Each 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 message | Save 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 files | The 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
- Functional: identify a client (user ID, IP address or API key); limits are configurable ("100 requests per minute"); over the limit → HTTP 429 with headers that say when to retry.
- Non-functional: the check adds < 10 ms; highly available; small counting errors are OK; 1 million requests per second.
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
| Problem | Chosen 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 client | Store 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 down | Decide 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]
- Requirements: send an email for each event type; never lose a notification; the UI must not wait.
- Why each box: queue = UI returns fast and jobs survive a restart; one job at a time = predictable load; separate email service = one job per service.
- Deep dives to offer: retries with backoff, a dead-letter queue for jobs that keep failing, idempotency (no duplicate emails), and what you would change at 100× the volume (more processors, a bigger queue such as SQS or Kafka).
8. Quick checklist for the interview
- Ask questions; write requirements and out of scope.
- Underline nouns; decide class vs field; name the orchestrator.
- Write classes in simple notation (fields, methods).
- Ask "real code or pseudocode?"; write the happy path, then edge cases.
- Walk one use case through the code, out loud.
- On a new requirement: show where it goes; use an interface / strategy for the part that varies.
- Say why you made each split.
System design (HLD):
- Top 3 functional requirements; non-functional with numbers; say what is below the line.
- Entities → API → simple boxes and arrows that work end to end.
- Senior: name the hard parts yourself and start the deep dives.
- For each choice, say the trade-off (what you gain, what you give up).
If they dig deeper
| They ask | You 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):
- Hello Interview, LLD delivery framework: https://www.hellointerview.com/learn/low-level-design/in-a-hurry/delivery
- Hello Interview, LLD introduction: https://www.hellointerview.com/learn/low-level-design/in-a-hurry/introduction
- Hello Interview, Parking Lot (requirements visible, solution paywalled): https://www.hellointerview.com/learn/low-level-design/problem-breakdowns/parking-lot
- Hello Interview, Amazon Locker: https://www.hellointerview.com/learn/low-level-design/problem-breakdowns/amazon-locker
- Hello Interview, System Design delivery framework: https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery
- Hello Interview, System Design introduction (levels): https://www.hellointerview.com/learn/system-design/in-a-hurry/introduction
- Hello Interview, problem list: https://www.hellointerview.com/learn/system-design/problem-breakdowns/overview
- Hello Interview breakdowns: Bitly, Ticketmaster, FB News Feed, WhatsApp, Uber, Rate Limiter (https://www.hellointerview.com/learn/system-design/problem-breakdowns/…)
- Common-question roundups: https://www.designgurus.io/answers/detail/what-are-reddit-system-design-interview-questions , https://dev.to/stack_overflowed/top-30-system-design-interview-questions-in-2026-35cc