System Design and LLD Interview Prep
Practise HLD, C++ LLD, design patterns, and realistic interview cases.
Introduction
30 Second Summary
Every senior engineering interview eventually lands on the same question: "Design a system that handles millions of users." Most developers freeze because they have never practiced thinking at that scale.
In this project, you will build reusable HLD and C++ LLD interview playbooks. You will practise architecture trade-offs, cloud patterns, concurrency, distributed coordination, capacity planning, object-oriented design, and realistic interview cases.
What You'll Build
You will finish with architecture diagrams, C++ class designs, and decision frameworks you can present under interview pressure.
By the end of this project, you'll have:
- An HLD decision playbook covering all 44 catalogued cloud patterns plus concurrency, consensus, time, capacity, hotspots, and multi-region trade-offs.
- A C++ LLD toolkit covering SOLID, UML, all 23 GoF patterns, ownership, concurrency, and testable class design.
- A case-study portfolio spanning chat, feeds, payments, media, parking, elevators, vending, notifications, and timed interview drills.
- Secret Mission: Scale a WhatsApp-style system to 2 billion users with regional routing, partitioning, backpressure, and failover.
Are there any prerequisites?
You should understand basic APIs, databases, and object-oriented programming. The LLD examples use modern C++17, so familiarity with classes, references, and STL containers will help.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Design Workspace
Before you can design a system, you need a canvas to think on. In system design interviews, your thinking happens visually on a whiteboard or diagram tool.
In this step, you will set up Eraser.io as your design workspace and get comfortable with its diagram-as-code feature. This is the same tool you will use throughout the project to draw architecture diagrams.
In this step, get ready to:
- Create a new file in Eraser.io called "System Design Practice".
- Explore the diagram-as-code feature and built-in icon library.
- Draw a warm-up test diagram connecting Client, Server, and Database.
Create a new file in Eraser.io
Every Eraser.io file is a workspace that combines a canvas for diagrams and a note editor for documentation. You will create one file to hold all your system design practice diagrams.
- Open your browser and navigate to eraser.io.
- Sign in to your Eraser.io account.
- On the dashboard, click create file.
- Name the file System Design Practice.
- Switch the layout to Canvas so you have the full drawing area visible.
What is an Eraser.io file?
An Eraser file has two parts: a Canvas for visual diagrams and a Note editor for Markdown documentation.
You can toggle between three layouts: Both, Note, or Canvas. For system design practice, you will mostly work on the Canvas.
You should now see an empty canvas ready for diagramming.
Explore the diagram-as-code feature and icon library
Eraser.io lets you create diagrams by writing simple code instead of dragging and dropping boxes. This is faster for system design work because you can describe complex architectures in a few lines.
- On the canvas, open the insert menu by pressing / on your keyboard.
- Select Diagram-as-code from the menu.
- Select Cloud Architecture as the diagram type.
- A code editor panel opens on the canvas where you type diagram code that Eraser automatically renders into visual boxes and arrows.
What is diagram-as-code?
Instead of manually positioning rectangles and drawing lines, you write short declarative statements. Eraser handles all the layout and styling for you.
The syntax is simple: define nodes by name, add optional properties in square brackets (like icons), and connect them with > arrows. For example, Server [icon: aws-ec2] creates a node labelled Server with an EC2 icon.
Eraser includes 3000+ built-in tech icons covering AWS, GCP, Azure, Kubernetes, and most tech logos. You never need to hunt for icons externally.
Draw your warm-up test diagram
Time to get your hands dirty. You will draw the simplest possible architecture: a client connecting to a server connecting to a database.
- Clear any placeholder code in the diagram editor.
- Type the following diagram code into the editor:
// Define nodes
Client
Server
Database [icon: database]
// Define connections
Client > Server > Database
What does this code do?
- Each line in the nodes section defines a box in your diagram. You can optionally add properties in square brackets, like [icon: database] to display a recognizable icon.
- The > symbol creates a directional connection (arrow) between nodes. Client > Server > Database means Client connects to Server, which connects to Database.
- Lines starting with // are comments that help you organize your code. Eraser ignores them when rendering the diagram.
- You should see three boxes appear on the canvas with arrows flowing from Client to Server to Database.
- Try clicking on individual nodes in the rendered diagram to see how you can reposition them.
Don't see your diagram rendering?
- Make sure you selected Cloud Architecture as the diagram type when inserting. Other types (like Sequence Diagram) use different syntax.
- Check that your node names and connection lines are on separate lines. Putting everything on one line can cause unexpected results.
- Verify that the > arrow has spaces on both sides (e.g. Client > Server not Client>Server).
Still stuck? Help me troubleshoot my Eraser.io diagram-as-code
✔️ Awesome, I see three connected boxes!
Great work. Your workspace is set up and you have confirmed that diagram-as-code works.
ⓧ I'd like to double check the full code
Here is the complete diagram code that should be in your editor:
// Define nodes
Client
Server
Database [icon: database]
// Define connections
Client > Server > Database
Your workspace is ready. Next up, you will learn what system design actually means and why interviewers test it.
Understand the Big Picture of System Design
Your workspace is ready and you've confirmed the diagram-as-code feature works. Now it's time to step back and understand what system design actually means.
Before you draw any architecture diagram, you need a mental model of what you're doing and why. This step gives you the foundational vocabulary and thinking that every later step builds on.
In this step, get ready to:
- Understand what system design is and why it matters at scale.
- Learn the difference between monolith and microservices architectures.
- Understand the format and expectations of a system design interview.
Understand what system design actually is
System design is the practice of deciding HOW to structure software so it handles millions of users without breaking. It is not about writing code.
It is about choosing the right components, connecting them correctly, and reasoning about what happens when traffic spikes from 100 users to 10 million.
What does a system designer actually decide?
A system designer answers questions like: Where should data live? How do we keep the app fast when a million people hit it at once? What happens if one server crashes?
You're choosing components (databases, caches, queues, load balancers) and connecting them in a way that handles real-world scale. Think of it like designing a city's road system before anyone starts pouring concrete.
Learn the difference between monolith and microservices
Every system starts with a fundamental architecture decision: do you keep everything in one application, or split it into many smaller services?
What is a monolith?
A monolith is one big application that handles everything. Your typical Express or Flask app is a monolith.
All the logic (user auth, orders, payments, notifications) lives in the same codebase and deploys as a single unit. It's simpler to develop and debug, but harder to scale individual pieces independently.
What are microservices?
Microservices break a system into independent services (Order Service, Payment Service, Notification Service) that each do one thing well.
These services communicate over APIs or message queues. You can scale, deploy, and update each service independently. The trade-off is added complexity in communication, monitoring, and deployment.
- In your System Design Practice file on Eraser.io, insert a new diagram-as-code block below your warm-up diagram by clicking the insert menu (+) and selecting Diagram-as-code, then Cloud Architecture Diagram.
- Paste the following code to visualise the difference between monolith and microservices:
// Monolith Architecture
Monolith App [icon: server] {
Auth [icon: lock]
Orders [icon: shopping-cart]
Payments [icon: credit-card]
Notifications [icon: bell]
}
Database [icon: database]
Monolith App > Database
// Microservices Architecture
API Gateway [icon: cloud]
Auth Service [icon: lock]
Order Service [icon: shopping-cart]
Payment Service [icon: credit-card]
Notification Service [icon: bell]
Message Queue [icon: mail]
Users DB [icon: database]
Orders DB [icon: database]
// Define connections
API Gateway > Auth Service, Order Service
Order Service > Message Queue
Message Queue > Payment Service, Notification Service
Auth Service > Users DB
Order Service > Orders DB
What does this diagram show?
The top section shows a monolith: everything lives inside one box and talks to a single database. Simple, but every part scales together and deploys together.
The bottom section shows microservices: each service is its own box with its own database. The API Gateway routes requests, and the message queue lets services communicate without being directly connected.
- Review the rendered diagram. You should see two distinct architectural patterns side by side: a single grouped box for the monolith and separate connected nodes for the microservices.
Diagram not rendering?
Make sure you selected Cloud Architecture Diagram (not Sequence Diagram or Flow Chart) when inserting the diagram-as-code block.
Check that each node name is unique. Eraser requires distinct names for every node in a diagram.
Help me fix my Eraser.io diagram-as-code rendering issue.
Understand what a system design interview looks like
Now that you know WHAT system design is, you need to understand the arena where you'll demonstrate it.
A system design interview is 45 minutes, typically on a whiteboard or diagramming tool. You drive the conversation. Nobody expects working code.
What do interviewers actually want to see?
They want to see you THINK like an architect. Can you break down a vague problem ("design Uber") into concrete requirements, estimate the scale, pick appropriate components, and reason about trade-offs?
The process matters more than the final diagram. An interviewer would rather see clear thinking with a simpler design than a complex diagram you can't explain.
The number one mistake to avoid
The biggest failure mode in system design interviews is jumping straight to drawing a diagram before clarifying requirements. Always start by asking questions about what the system needs to do, who uses it, and at what scale.
- Here's a summary of the interview format to keep in mind:
- Duration: 45 minutes total.
- Format: you diagram and explain your design on a whiteboard or tool like Eraser.io.
- Expectation: no working code. Demonstrate that you can reason about scale, pick components, and discuss trade-offs.
- Who drives: you do. The interviewer gives you a prompt ("Design WhatsApp") and you lead from there.
You now have the big picture: system design is about choosing and connecting components at scale, and interviews test your ability to think through that process out loud. Next up, you'll learn the exact 6-step framework that gives you a repeatable structure for any system design question.
Learn the 6-Step Interview Framework
You now know what system design is and what an interview expects from you. The next question is: how do you actually structure your answer when someone says "Design WhatsApp" and starts a 45-minute timer?
In this step, you'll learn a repeatable 6-step framework that works for any system design question. You'll also learn the quick math (back-of-envelope estimation) that tells you whether your system needs 1 server or 1,000.
In this step, get ready to:
- Learn the 6-step framework with time allocations for a 45-minute interview.
- Understand back-of-envelope estimation constants.
- Practice the estimation template with a worked example.
Learn the 6-step framework
Every great system design answer follows the same skeleton. Different sources call it different names (Alex Xu's 4-step, RESHADED, PEDALS), but they all boil down to six phases with a fixed time budget.
Here is the framework and its time allocation for a standard 45-minute interview:
- Step 1: Clarify Requirements (5 min). Ask questions. Nail down functional requirements (what the system does) and non-functional requirements (scale, latency, availability).
- Step 2: Estimate Capacity (5 min). Quick math to figure out QPS, storage, and bandwidth. This tells you what class of architecture you need.
- Step 3: API Design (5 min). Define the key endpoints or interfaces. This grounds the system in concrete inputs/outputs.
- Step 4: High-Level Architecture (15 min). Draw the boxes and arrows. This is where the diagram lives.
- Step 5: Deep Dive (10 min). Pick 1-2 components and explain them in detail. Show you understand the hard parts.
- Step 6: Trade-offs (5 min). Discuss alternatives and justify your choices. Every design decision has a cost.
Why does the framework start with questions, not diagrams?
Roughly 50% of failed system design interviews trace back to candidates jumping straight to a diagram before clarifying requirements. The interviewer wants to see your process. The final diagram matters less than how you arrived at it.
Always state your assumptions out loud. If you assume 10M daily users, say so. The interviewer can correct you before you build on a wrong foundation.
Understand back-of-envelope estimation
Step 2 of the framework asks you to estimate capacity. You are not solving for precision here. You need order-of-magnitude numbers that tell you whether you are building for one server or a distributed cluster.
Memorise these constants. They come up in nearly every estimation:
- 1 day ≈ 100K seconds (actual: 86,400). Round up for quick division.
- Single Redis server: 100K+ QPS (GET/SET operations in memory).
- Single PostgreSQL server: 5K-10K QPS (simple indexed queries).
- Peak traffic = 3-5x average for consumer apps.
- Single server: 10K-50K concurrent connections (with async I/O).
What is the estimation's job?
The estimate picks the architecture class. If your QPS is 500, a single PostgreSQL server handles it easily. If your QPS is 500,000, you need caching, sharding, and likely a CDN.
As a rule of thumb: need more than 50K QPS? You need caching or sharding. Need more than 1 TB storage? You need sharding. Need less than 1ms latency? You need caching. Need 99.99%+ availability? You need multi-region.
Practice the estimation template and document your cheat sheet
Every estimation follows the same four-step template: DAU → QPS → Storage → Bandwidth. Let's walk through a concrete example so you can see the pattern in action.
Imagine you are designing a messaging system with 50 million daily active users, each sending 10 messages per day:
- Total messages/day = 50M users × 10 messages = 500M messages/day.
- Average QPS = 500M ÷ 100K seconds = ~6,000 QPS.
- Peak QPS = 6,000 × 3 = ~18,000 QPS.
- What does 18K QPS tell us? A single PostgreSQL server maxes out at ~10K QPS. So we need either caching in front of the database or multiple database replicas to handle reads.
The formula to memorise
Average QPS = DAU × requests_per_user_per_day ÷ 86,400 (or just divide by 100K for quick math). Peak QPS = Average QPS × peak_factor (use 3x for consumer apps).
Now let's document this as a cheat sheet you can reference during practice. You'll add this to your Eraser.io file alongside your warm-up diagram.
- In your System Design Practice file on Eraser.io, click the + button to add a new section (or scroll below your existing diagram).
- Add the following as your personal system design cheat sheet. Type or paste this into a text/doc section:
# System Design Interview Framework
## The 6 Steps (45 min total)
1. Clarify Requirements (5 min) - Ask questions. Functional + non-functional.
2. Estimate Capacity (5 min) - DAU → QPS → Storage → Bandwidth.
3. API Design (5 min) - Key endpoints with inputs/outputs.
4. High-Level Architecture (15 min) - Draw the boxes and arrows.
5. Deep Dive (10 min) - Pick 1-2 hard parts. Explain in detail.
6. Trade-offs (5 min) - Justify choices. Name alternatives.
## Estimation Constants
- 1 day ≈ 100K seconds (actual: 86,400)
- Redis: 100K+ QPS (in-memory GET/SET)
- PostgreSQL: 5K-10K QPS (indexed queries)
- Peak traffic: 3-5x average (consumer apps)
- Single server: 10K-50K concurrent connections (async I/O)
## Estimation Template
Avg QPS = DAU × requests_per_user / 100K
Peak QPS = Avg QPS × 3 (consumer apps)
## Scaling Thresholds
- >50K QPS → need caching or sharding
- >1 TB storage → need sharding
- <1ms latency → need caching
- >99.99% availability → need multi-region
## Worked Example: Messaging App
- 50M DAU × 10 messages = 500M messages/day
- Avg QPS = 500M / 100K = ~6,000
- Peak QPS = 6,000 × 3 = ~18,000
- Conclusion: single DB won't handle peak → need caching or read replicas
What does this cheat sheet do?
This is your quick-reference card for interviews. The 6 steps keep you on track so you never freeze. The estimation constants let you do capacity math in your head. The scaling thresholds tell you which components to reach for.
You will use this framework in Step 6 (food delivery platform) and Step 7 (WhatsApp design). Every time you practice a new problem, follow these 6 steps in order.
- Confirm your cheat sheet is saved in your System Design Practice file. You should see the framework, constants, and worked example all visible.
Running out of space in your file?
Eraser.io's free plan supports up to 3 files. If your current file feels cluttered, you can create a separate file called "Cheat Sheet" for your reference notes and keep System Design Practice for diagrams only.
Manual diagram creation is unlimited on the free plan. Only AI-generated diagrams are capped.
Help me organise my Eraser.io files for system design practice.
You now have a structured process for answering any system design question and the mental math to size a system in seconds. Next up, you'll learn the actual building blocks (load balancers, caches, queues) that slot into Step 4 of the framework.
Learn Core Building Blocks: Infrastructure
Your framework now tells you which questions to ask. The next challenge is moving each request safely from a user to the correct service.
This step builds the traffic path from DNS to the edge, load balancer, gateway, and origin. You will separate routing, protection, and delivery responsibilities.
In this step, get ready to:
- Trace requests through DNS and traffic-routing layers.
- Compare load balancers, reverse proxies, and API gateways.
- Place edge security and content delivery controls.
Route requests to healthy services
Follow the traffic path
DNS maps a hostname to a reachable endpoint. Geo-routing or latency-based routing can steer users toward a region before Layer 4 or Layer 7 load balancers distribute traffic within it.
Layer 4 routing uses connection metadata. Layer 7 routing can inspect hosts, paths, headers, and cookies, which enables service-aware decisions.
- Use health checks to remove failed instances from rotation.
- Use connection draining to finish in-flight work before removing an instance.
- Use sticky sessions only when state cannot be externalised without unacceptable cost.
Separate edge responsibilities
- Use Gateway Routing to send one public endpoint to the correct backend by host, path, header, or policy.
- Use Gateway Aggregation to combine several backend calls when reducing client round trips matters.
- Use Gateway Offloading for shared work such as TLS termination, authentication checks, compression, or quotas.
- Use a Gatekeeper to validate and sanitise untrusted requests before forwarding them to private services.
- Use a web application firewall to filter common application attacks.
- Use DDoS protection and edge rate limits before traffic reaches scarce application capacity.
Place content at the edge
- Use Static Content Hosting to serve immutable assets directly from object storage or a specialised edge platform.
- Serve cacheable assets from a CDN point of presence near the user.
- Keep an origin server or object store as the authoritative source.
- Add an origin shield when many edge locations would otherwise miss against the origin at once.
- Draw the complete request path and label each layer with one responsibility.
Your request path now has clear routing and protection boundaries. Next, you will choose how services run and scale behind that edge.
Choose Compute and Service Patterns
Your edge can now route requests to healthy capacity. The services behind it still need clear boundaries, scalable execution, and safe deployment paths.
This step compares compute and service patterns without assuming that microservices are the default. You will begin with the simplest architecture that satisfies isolation and scaling needs.
In this step, get ready to:
- Compare vertical and horizontal compute scaling.
- Choose service boundaries and discovery patterns.
- Plan low-risk deployments and migrations.
Scale compute deliberately
- Scale vertically when simplicity and shared state matter more than elastic growth.
- Scale horizontally when requests can be distributed across stateless instances.
- Choose autoscaling signals from CPU, latency, concurrency, or queue depth.
- Keep warm capacity when cold starts would violate latency targets.
Choose service boundaries
Start with the simplest boundary
A modular monolith keeps deployment simple while preserving internal boundaries. Microservices add independent scaling and failure isolation, but they also add network, data, deployment, and operational complexity.
- Use service discovery when instance addresses change dynamically.
- Use database-per-service when independent ownership outweighs cross-service query cost.
- Use Backend for Frontend when web, mobile, or partner clients need different aggregation and latency choices.
- Use Sidecar or Ambassador when cross-cutting network behaviour should evolve outside the service process.
- Use an Anti-Corruption Layer when a legacy or external model must not leak into the new domain.
- Use Compute Resource Consolidation only when compatible workloads can share capacity without losing isolation.
Deploy and migrate safely
- Compare rolling, blue-green, and canary releases by rollback speed and blast radius.
- Use feature flags to separate deployment from release.
- Use an External Configuration Store when operational values must change without rebuilding the service.
- Use Deployment Stamps when one repeatable scale unit should contain compute, data, quotas, and failure isolation.
- Use the Strangler Fig pattern to replace a legacy boundary incrementally.
- Draw a compute topology with its scale unit, service boundary, configuration path, and deployment strategy.
Your services now have a reasoned topology and release path. Next, you will choose how clients and services communicate.
Choose APIs and Communication Protocols
Your services have clear boundaries, but those boundaries only work when communication matches the workload. A chat connection, public API, and internal service call need different protocols.
This step compares synchronous, asynchronous, request-response, and real-time communication. You will also design APIs that remain safe as clients and schemas evolve.
In this step, get ready to:
- Compare REST, GraphQL, and gRPC.
- Choose polling, events, webhooks, or persistent connections.
- Design versioned and idempotent APIs.
Choose a request-response protocol
- Use REST for resource-oriented public APIs and broad client compatibility.
- Use GraphQL when clients need flexible projections across a controlled graph.
- Use gRPC for typed internal calls where efficient binary transport and generated clients help.
- Use asynchronous request-reply when processing outlives an interactive timeout.
Choose a real-time model
- Use short polling for infrequent updates with simple clients.
- Use long polling when updates are sporadic and persistent connections are unavailable.
- Use Server-Sent Events for one-way server streams.
- Use WebSockets for bidirectional low-latency interaction.
- Use webhooks for server-to-server event delivery with retries and signature validation.
Make APIs safe to evolve
- Version incompatible contracts while keeping additive changes backward compatible.
- Use cursor pagination when records change during traversal.
- Require idempotency keys for retried side-effecting requests.
- Carry deadlines across service calls so abandoned work stops consuming capacity.
- Use a Messaging Bridge when independently owned brokers or protocols must exchange messages during integration or migration.
- Draw one communication decision table and one end-to-end sequence.
Your communication choices now follow client behaviour, latency, and delivery needs. Next, you will select storage from the queries and guarantees the system needs.
Learn Core Building Blocks: Data Layer
Traffic and compute choices only matter when the data layer can support the workload. Your storage model controls which queries stay simple and which guarantees remain affordable.
This step turns database selection into a requirements decision. You will choose from relational, key-value, document, wide-column, graph, search, time-series, analytical, and object storage models.
In this step, get ready to:
- Match access patterns to storage models.
- Compare transaction, schema, latency, and scale requirements.
- Model indexes, denormalisation, and data lifecycles.
Compare storage models
Choose by access pattern
Relational databases fit transactions, joins, and enforced relationships. Key-value, document, wide-column, graph, search, time-series, analytical, and object stores optimise different access patterns.
- Relational: orders, payments, inventory, and workflows that need multi-row transactions.
- Key-value or document: sessions, profiles, carts, and aggregate-shaped records.
- Wide-column or time-series: high-volume events grouped by a stable partition key.
- Graph, search, or analytical: relationship traversal, relevance queries, and large aggregations.
- Object storage: media, backups, exports, and immutable files.
Test the decision criteria
- Write the highest-volume reads and writes before naming a database.
- Mark which operations require atomic transactions or strong read-after-write behaviour.
- Estimate record size, retention, growth, and the expected read-to-write ratio.
- Identify search, geospatial, graph, and analytical queries that need specialised indexes.
- Choose polyglot persistence only when one storage model creates a clear bottleneck.
Model data for the workload
- Normalise transactional data when updates must stay consistent.
- Denormalise read models when a repeated join dominates latency.
- Use an Index Table when a separate lookup structure can serve a query the primary key cannot answer efficiently.
- Use a Materialized View when an expensive projection can refresh asynchronously and serve repeated reads.
- Add primary, secondary, and composite indexes that match real query predicates.
- Define hot, warm, and cold tiers for different retention and access needs.
- Draw your storage decision matrix in Eraser and label one accepted trade-off per store.
Your storage choices now follow the workload instead of a product preference. Next, you will scale those choices with replication and partitioning.
Scale Data with Replication and Partitioning
Your data store matches the workload. Growth now creates pressure on one node, one partition, and one failure domain.
This step separates replication from partitioning. You will use each pattern for the bottleneck it actually solves.
In this step, get ready to:
- Compare replication topologies and lag.
- Choose partition strategies and shard keys.
- Reason about cross-shard operations.
Replicate for availability and reads
- Compare primary-replica, multi-primary, synchronous, and asynchronous replication.
- Route stale-tolerant reads to replicas.
- Keep read-after-write traffic on a path that can observe the latest acknowledged write.
- Plan promotion, failover, and protection from split-brain writes.
Partition for capacity
- Use range partitioning for locality when hot ranges can be controlled.
- Use hash partitioning for even distribution when range scans are less important.
- Use geographic or directory partitioning when placement follows residency or tenancy.
- Use consistent hashing and virtual nodes when membership changes frequently.
- Choose a high-cardinality shard key that avoids hot partitions.
Expose distributed query costs
- Identify scatter-gather queries before accepting a shard key.
- Avoid cross-shard joins and transactions on the hottest path.
- Plan resharding before partitions reach storage or throughput limits.
- Draw one replicated and partitioned store with its failure and rebalancing paths.
Your data layer can now grow across nodes without hiding its distributed costs. Next, you will decide which consistency guarantees the product truly needs.
Reason About Consistency and Coordination
Replication and partitioning create multiple views of data. The next design decision is which observations must agree and how much coordination the system can afford.
This step turns consistency into a per-operation requirement. You will compare models, partition trade-offs, quorums, and coordination mechanisms.
In this step, get ready to:
- Match operations to consistency models.
- Apply CAP and PACELC without slogans.
- Choose coordination only when invariants require it.
Specify consistency per operation
- Use strong consistency for invariants such as unique ownership or final seat allocation.
- Use eventual consistency for replicas, feeds, counters, and derived views that can converge.
- Consider read-your-writes, monotonic reads, causal, and session guarantees when global strength is unnecessary.
Reason about distributed trade-offs
Use CAP at the right moment
CAP asks what the system does during a network partition. PACELC adds the normal-case choice between latency and consistency when no partition exists.
- Use read and write quorums to reason about overlap, latency, and availability.
- Define conflict resolution instead of assuming last-write-wins preserves intent.
- Use sequence or version metadata when ordering affects correctness.
Coordinate safely
- Use leader election when one active coordinator must own a responsibility.
- Use leases with fencing tokens when stale owners could continue writing.
- Use distributed locks only when redesigning the invariant around ownership or idempotency is insufficient.
- Create a consistency table for payments, profiles, presence, feeds, and analytics.
You can now name the exact guarantee each operation needs. Next, you will reduce latency and load without serving uncontrolled stale data.
Master Caching and CDN Patterns
Your consistency requirements define which stale reads are acceptable. Caching can now reduce latency and origin load without quietly violating product correctness.
This step covers application caches and CDN delivery as one hierarchy. You will design population, invalidation, eviction, and stampede protection together.
In this step, get ready to:
- Compare cache placement and population patterns.
- Control staleness, eviction, and hot keys.
- Choose pull, push, and private CDN delivery.
Choose a cache pattern
- Use cache-aside when the application can manage misses and invalidation.
- Use read-through when the cache abstraction owns loading.
- Use write-through when immediate cache freshness justifies write latency.
- Use write-behind only when deferred persistence and loss risk are acceptable.
- Use refresh-ahead for predictable hot data with stable refresh cost.
Control cache failure modes
- Set TTLs from acceptable staleness and refresh cost.
- Use versioned keys or event-driven invalidation for changed entities.
- Use request coalescing, jittered expiry, or early refresh to stop stampedes.
- Use negative caching to protect the origin from repeated misses.
- Replicate or split hot keys when one entry concentrates traffic.
Choose a CDN delivery model
- Pull CDN: fetch an asset from the origin after the first edge miss.
- Push CDN: pre-position selected assets before requests arrive.
- Use cache keys and request variation rules that prevent one user response from leaking to another.
- Use signed URLs or cookies for private content.
- Draw a cache hierarchy with hit, miss, expiry, invalidation, and origin-shield paths.
Your cache hierarchy now has correctness and failure policies. Next, you will decouple work with queues, pub/sub, streams, and fan-out.
Design Queues, Pub/Sub, and Fan-Out
Caching accelerates repeated reads, but it does not decouple producers from slow or bursty work. Messaging gives services a buffer and a delivery contract.
This step distinguishes work queues, pub/sub, and event streams. You will also compare push, pull, and feed fan-out models.
In this step, get ready to:
- Choose queues, pub/sub, or streams.
- Compare push and pull consumers.
- Design fan-out for subscribers and feeds.
Choose the messaging model
- Use a point-to-point queue when one worker should claim each task.
- Use Competing Consumers to scale independent workers against the same queue.
- Use Publisher-Subscriber when multiple subscribers need their own copy of an event.
- Use an event stream when ordered partitions, replay, and retained history matter.
- Use Queue-Based Load Leveling to absorb bursts before constrained dependencies.
- Use a Priority Queue when urgent or high-value work must overtake ordinary work.
Compare push and pull delivery
- Use broker push when low delivery latency matters and consumers can advertise or enforce capacity.
- Use consumer pull when workers need direct control over batch size, rate, and acknowledgement.
- Use long polling to reduce empty requests without holding permanent connections.
- Track queue depth and consumer lag as overload signals.
Design fan-out
- Use topic or content-based routing to fan an event out to independent subscribers.
- Fan-out on write: precompute recipient timelines for fast reads.
- Fan-out on read: merge source content when a reader opens the feed.
- Hybrid fan-out: push ordinary accounts and pull celebrity content.
- Draw one messaging decision matrix and one fan-out flow.
You can now select the right asynchronous model and fan-out direction. Next, you will make that event processing reliable under duplicates, retries, and partial failure.
Build Reliable Event Processing
Your messaging topology can move work independently. Reliability now depends on what happens when messages repeat, arrive late, fail, or outlive a consumer.
This step defines delivery, ordering, retry, and replay behaviour explicitly. You will design consumers that remain correct under at-least-once delivery.
In this step, get ready to:
- Compare delivery guarantees and acknowledgements.
- Handle duplicates, retries, and poison messages.
- Design ordered and replayable streams.
Define the delivery contract
- Use at-most-once when loss is safer than duplication.
- Use at-least-once when consumers can make repeated delivery harmless.
- Treat exactly-once claims as a scoped coordination guarantee rather than a universal property.
- Set acknowledgement and visibility timeouts longer than normal processing but short enough for recovery.
Make consumers safe
- Use idempotency keys or processed-message records to suppress duplicate effects.
- Retry transient failures with exponential backoff and jitter.
- Move exhausted or malformed messages to a dead-letter queue.
- Use Quarantine when inputs from an external or untrusted source need isolated validation before they enter the trusted processing path.
- Store large payloads externally and pass a reference with the Claim Check pattern.
Preserve order and replay
- Choose a partition key that groups events requiring relative order.
- Use Sequential Convoy to process each related message group in order while other groups progress concurrently.
- Track sequence numbers when clients must detect gaps or reordering.
- Use Pipes and Filters when independent processing stages need separate scaling, retry, or reuse.
- Store consumer offsets and retention long enough for recovery and backfills.
- Version event schemas with backward-compatible consumers.
- Draw the retry, dead-letter, replay, ordering, and deduplication paths.
Your consumers now tolerate duplicate and delayed delivery. Next, you will coordinate business workflows that cross service and database boundaries.
Coordinate Distributed Workflows
Reliable events protect individual messages, but a business operation can still span several services and fail halfway through. Distributed workflows need explicit state, compensation, and recovery.
This step covers sagas, outboxes, CQRS, and event sourcing as separate tools. You will use each only when its added state and operational cost solve a named problem.
In this step, get ready to:
- Coordinate multi-service transactions with sagas.
- Prevent dual writes with outbox and inbox patterns.
- Separate write models, read models, and event history.
Model a saga
- Use orchestration when one coordinator should own workflow state and recovery.
- Use Choreography when bounded services can react to events without hidden cycles.
- Use a Saga when one business transaction spans independently committed service operations.
- Use a Compensating Transaction to reverse or offset every reversible completed step.
- Use Scheduler Agent Supervisor when distributed tasks need assignment, progress tracking, and reassignment after failure.
- Record timeouts, retries, and manual intervention states.
Eliminate dual-write gaps
- Write business state and an outbox record in one local transaction.
- Publish committed outbox records through a relay or change-data-capture process.
- Use an inbox or idempotency record before applying a consumed event.
- Correlate every workflow message with the business operation.
Choose CQRS or event sourcing carefully
- Use CQRS when read and write models have genuinely different scaling or shape requirements.
- Use materialised views to serve derived reads from events.
- Use event sourcing when the event history is the authoritative state and replay has product value.
- Use snapshots to bound replay time.
- Draw a saga with outbox, compensation, and recovery states.
Your multi-service workflows now expose partial failure and recovery. Next, you will protect every dependency with timeouts, isolation, and disaster-recovery plans.
Design for Failure and Recovery
Your workflows can recover from expected partial failures. Production systems must also survive slow dependencies, correlated outages, and the loss of an entire failure domain.
This step layers dependency protection, high availability, and disaster recovery. You will distinguish keeping service available from restoring lost state.
In this step, get ready to:
- Protect calls with deadlines, retries, and circuit breakers.
- Design redundancy and graceful degradation.
- Define backup, failover, RTO, and RPO targets.
Contain dependency failures
- Set timeouts and end-to-end deadlines before adding retries.
- Retry only transient and idempotent operations with backoff and jitter.
- Open circuit breakers when repeated failures would waste capacity.
- Use bulkheads to stop one dependency or tenant from exhausting every worker.
- Provide fallbacks that preserve truthful behaviour.
Design high availability
- Remove single points of failure across compute, routing, storage, and control planes.
- Distribute replicas across independent failure domains.
- Use Health Endpoint Monitoring to expose liveness, readiness, and dependency state for automated routing and recovery.
- Use active-active or active-passive topology according to consistency and cost.
- Use load shedding and degraded modes before saturation becomes total failure.
Plan disaster recovery
- Set Recovery Time Objective from tolerated downtime.
- Set Recovery Point Objective from tolerated data loss.
- Test backups with restores rather than trusting successful backup jobs.
- Document failover, traffic evacuation, and failback steps.
- Draw a failure map and label the response to each failure.
Your architecture now contains failures instead of assuming them away. Next, you will defend capacity and data from abuse, overload, and unauthorised access.
Protect Systems from Abuse and Overload
Reliability mechanisms keep dependencies from amplifying failures. The system must also decide who may act and how much work each caller may consume.
This step combines rate limiting, overload control, and security boundaries. You will place each control close to the resource or trust boundary it protects.
In this step, get ready to:
- Compare rate-limiting algorithms.
- Control overload with admission and backpressure.
- Layer identity, transport, data, and audit security.
Choose a rate limiter
- Use fixed windows for simple coarse limits with known boundary bursts.
- Use sliding logs for precision when memory cost is acceptable.
- Use sliding counters for a practical accuracy and cost balance.
- Use token buckets for controlled bursts.
- Use leaky buckets for a steady processing rate.
Control overload
- Apply per-user, per-IP, per-tenant, endpoint, and global budgets where fairness differs.
- Reject excess work through admission control before it consumes scarce dependencies.
- Use bounded queues, priorities, backpressure, and load shedding to protect critical traffic.
- Return retry guidance only when the service can recover before another attempt.
Layer security controls
- Separate authentication from authorisation.
- Use Federated Identity when an external identity provider should manage sign-in and trusted claims.
- Use sessions, OAuth 2.0, OpenID Connect, RBAC, or ABAC according to identity and policy needs.
- Use a Gatekeeper to validate and sanitise requests before they reach private backends.
- Use a Valet Key to grant time-limited direct access to one protected resource without proxying the transfer.
- Use TLS, mutual TLS, encryption at rest, managed secrets, and key rotation.
- Apply least privilege and record auditable security events.
- Draw a trust-boundary diagram with rate, identity, and resource-access controls.
Your system now protects trust boundaries and scarce capacity. Next, you will measure whether those controls and services meet their reliability targets.
Make Distributed Systems Observable
Protection controls are useful only when operators can see saturation, failures, and user impact. Observability turns internal signals into evidence for design and incident decisions.
This step combines logs, metrics, traces, service objectives, and capacity signals. You will choose telemetry from questions the team must answer.
In this step, get ready to:
- Design logs, metrics, and distributed traces.
- Define SLIs, SLOs, and error budgets.
- Create actionable dashboards and alerts.
Connect telemetry
- Use structured logs for discrete events and diagnostic context.
- Use metrics for trends, rates, percentiles, and saturation.
- Use traces to follow latency and failure across service boundaries.
- Propagate correlation and trace IDs across synchronous and asynchronous calls.
Define reliability targets
- Choose Service Level Indicators that reflect user-visible success, latency, freshness, or durability.
- Set Service Level Objectives from product expectations and engineering cost.
- Use error budgets to balance reliability work with change velocity.
- Track tail latency instead of relying on averages.
Operate from signals
- Build dashboards around traffic, errors, latency, saturation, queue lag, and dependency health.
- Alert on actionable symptoms with enough context to identify the affected journey.
- Forecast capacity from growth, headroom, and failure-mode demand.
- Link alerts to runbooks and incident timelines.
- Draw an observability map for one critical request.
Your system now produces evidence about user impact and capacity. Next, you will extend its routing, data, and recovery decisions across regions.
Design Global and Multi-Region Systems
Your architecture is observable within one region. Global users and regional failures now force decisions about placement, replication, residency, and conflict.
This step compares active-active and active-passive regions. You will map users, data, events, and recovery paths without promising impossible consistency and latency.
In this step, get ready to:
- Choose a regional traffic topology.
- Place global data and event flows.
- Plan regional failure and failback.
Route users to regions
- Use geo-routing or latency-based DNS to select a healthy region.
- Use the Geode pattern when every geographic deployment can serve local clients as an active copy.
- Use active-active regions for lower latency and failover when data conflicts are manageable.
- Use active-passive regions when simpler ownership and consistency outweigh idle cost.
- Preserve regional affinity when moving sessions or data would create correctness problems.
Place global data
- Use home-region ownership when one region should serialise writes for an entity.
- Use regional shards when tenants or users have stable geographic placement.
- Use cross-region replicas or global tables according to write and conflict requirements.
- Respect data-residency and deletion obligations in placement decisions.
- Relay events across regions with loop prevention and replay controls.
Recover from regional failure
- Define traffic evacuation and dependency readiness before declaring failover complete.
- Use multi-CDN or alternate origins when edge or origin providers fail.
- Prevent split-brain writes during uncertain regional ownership.
- Test failback as carefully as failover.
- Draw a two-region topology with normal, failover, and conflict paths.
Your design now accounts for distance, residency, and regional failure. Next, you will protect shared state when many requests race to change it.
Control Concurrent Updates and Shared State
Your architecture can now serve users across regions and recover from regional failure. Shared records still become unsafe when many requests modify them at the same time.
This step gives you a concurrency decision tree. You will match each invariant to atomic updates, version checks, locks, or serialized processing.
In this step, get ready to:
- Recognise race conditions and duplicate effects.
- Choose database and application concurrency controls.
- Protect distributed work without unsafe locks.
Identify the concurrency failure
Concurrency is a correctness requirement
Concurrency begins when two operations overlap on shared state. The design must preserve the business rule even when their reads and writes interleave.
- Trace a lost update when two writers read the same version before saving different results.
- Trace a check-then-act race when availability changes between validation and reservation.
- Trace write skew when separate rows pass local checks but violate a cross-record invariant.
- Trace duplicate effects when a timeout causes the caller to repeat a completed operation.
- State the invariant before selecting a concurrency mechanism.
Choose local concurrency control
- Use database uniqueness and check constraints when the data store can enforce the invariant atomically.
- Use an atomic update or compare-and-swap operation for counters and conditional state transitions.
- Use optimistic concurrency with a version value when conflicts are uncommon.
- Use pessimistic locking when conflicts are frequent and waiting is safer than retrying.
- Use multi-version concurrency control to let readers observe a consistent snapshot while writers progress.
Coordinate distributed work safely
- Use an idempotency key to make a repeated command return the original result without repeating its side effect.
- Serialize work by entity key when operations for one account, order, or seat require strict order.
- Use a lease when ownership must expire after a worker stops renewing it.
- Add a fencing token when a stale lease holder could still reach the protected resource.
- Prefer partition ownership or single-writer routing when it removes the need for a global lock.
- Create a concurrency matrix for seat booking, inventory decrement, payment creation, and profile editing.
You can now preserve invariants under overlapping requests. Next, you will coordinate ownership, ordering, and conflict resolution across distributed nodes.
Coordinate Distributed Nodes and Time
Your concurrency matrix protects one shared operation at a time. A distributed system also needs nodes to agree on ownership and interpret events that arrive without a perfect global clock.
This step separates coordination, ordering, and conflict resolution. You will explain what requires agreement and what can converge without it.
In this step, get ready to:
- Choose ownership and agreement mechanisms.
- Order events without trusting wall-clock time.
- Resolve concurrent changes deliberately.
Establish ownership and agreement
- Use leader election when one active node must own scheduling, metadata, or write coordination.
- Require a quorum when a decision must survive minority node failure.
- Explain consensus as agreement on an ordered history despite failures.
- Describe Raft and Paxos at interview depth by focusing on leaders, majorities, terms, and committed entries.
- Prevent split-brain by requiring exclusive write authority before accepting mutations.
Order events and generate identifiers
- Treat wall-clock timestamps as approximate because nodes can drift or pause.
- Use a logical clock when relative event order matters more than real time.
- Use a vector clock when the system must distinguish causality from concurrent updates.
- Use sequence numbers within a partition when consumers must detect gaps.
- Choose random, time-sortable, or coordinated identifiers according to ordering, predictability, and collision requirements.
Resolve conflicts with minimal coordination
- Use deterministic conflict rules only when losing one concurrent intent is acceptable.
- Merge commutative operations when their order does not change the result.
- Use a conflict-free replicated data type when replicas must merge supported concurrent changes without central coordination.
- Require coordination when a global invariant cannot be repaired after convergence.
- Draw a failure timeline that shows leader loss, election, stale ownership, and recovery.
You can now explain ownership and ordering under partial failure. Next, you will quantify demand and scale the component that reaches its limit first.
Estimate Capacity and Scale Bottlenecks
Your nodes can now coordinate without relying on a perfect clock. The next interview challenge is deciding what must scale before adding components to the diagram.
This step turns vague scale into estimates and bottlenecks. You will connect traffic, latency, storage, skew, and tenant isolation to specific scaling decisions.
In this step, get ready to:
- Estimate traffic, concurrency, storage, and bandwidth.
- Scale the first constrained resource.
- Handle hotspots and unfair workloads.
Build a capacity estimate
- Estimate average requests per second from active users and actions per user.
- Apply a peak factor instead of designing only for the daily average.
- Split traffic into reads, writes, uploads, downloads, and background work.
- Estimate in-flight concurrency with Little's Law by multiplying arrival rate by average time in the system.
- Estimate storage growth from records, media size, replicas, indexes, and retention.
- Estimate network throughput from payload size and peak request rate.
Scale the constrained resource
- Scale stateless request handling horizontally when load can be distributed safely.
- Scale CPU-bound work with more cores, workers, or partitions according to parallelism.
- Scale memory-bound work by reducing working sets, adding cache capacity, or partitioning ownership.
- Scale storage throughput with replicas, partitioning, batching, and access-pattern changes.
- Choose autoscaling signals from latency, in-flight work, queue depth, saturation, or consumer lag.
- Define a scale unit that packages compute, storage, quotas, and failure isolation.
Control skew and tenant pressure
- Detect hot keys and hot partitions through per-key or per-shard traffic measurements.
- Split, replicate, cache, or isolate a hot key according to its read and write behaviour.
- Use hybrid fan-out when a small number of high-follower accounts dominate write amplification.
- Apply per-tenant quotas and bulkheads to contain noisy neighbours.
- Use bounded queues and backpressure before downstream latency collapses.
- Create a capacity sheet for chat, payments, video, and news-feed workloads.
You can now defend a scaling plan with numbers and bottlenecks. Next, you will close the remaining HLD pattern gaps and organise the full catalog.
Complete the HLD Pattern Catalog
Your scaling plan now connects demand to concrete bottlenecks. A complete interview toolkit also needs the less common patterns that appear when boundaries, integration, security, or operations become the main constraint.
This step closes those gaps with an official cloud-pattern catalog. You will group every pattern by the problem it solves before using the next step to practise selection.
In this step, get ready to:
- Complete the edge and service pattern family.
- Complete the data and messaging pattern family.
- Complete the workflow and reliability pattern family.
Group edge, service, and operational patterns
- Gateway Routing, Gateway Aggregation, and Gateway Offloading separate routing, call composition, and shared gateway work.
- Backend for Frontend, Ambassador, and Sidecar place client-specific or cross-cutting behaviour beside the core service.
- Gatekeeper, Valet Key, and Federated Identity move validation, restricted resource access, or authentication to a controlled boundary.
- Static Content Hosting, Deployment Stamps, and Geode scale delivery through specialised hosting, repeatable scale units, or geographic copies.
- Anti-Corruption Layer, Strangler Fig, and External Configuration Store isolate legacy models, migrate incrementally, or change configuration outside deployments.
- Compute Resource Consolidation increases utilisation by placing compatible work on shared capacity with explicit isolation.
Group data and messaging patterns
- Cache-Aside, Index Table, Materialized View, and Sharding optimise repeated reads, lookup paths, derived queries, or data capacity.
- CQRS and Event Sourcing separate read and write models or preserve state as an append-only history.
- Asynchronous Request-Reply, Publisher-Subscriber, and Messaging Bridge decouple completion, distribute events, or connect incompatible brokers.
- Queue-Based Load Leveling, Competing Consumers, and Priority Queue buffer bursts, parallelise work, or protect urgent tasks.
- Pipes and Filters and Claim Check split processing stages or remove large payloads from the message path.
- Idempotent Consumer, Sequential Convoy, and Quarantine control duplicate effects, per-group order, or untrusted inputs.
Group workflow and reliability patterns
- Retry, Circuit Breaker, Bulkhead, and Health Endpoint Monitoring recover transient calls, stop harmful calls, isolate capacity, or expose readiness.
- Rate Limiting and Throttling control demand before a protected resource saturates.
- Leader Election and Scheduler Agent Supervisor assign ownership or redistribute work according to state and capacity.
- Choreography, Saga, and Compensating Transaction coordinate multi-service workflows and recover completed steps.
- Create an inventory with columns for problem, trigger, trade-off, failure mode, rejected alternative, and confidence.
- Mark every catalog pattern as interview-ready or needing review.
You now have the full HLD pattern catalog plus the concurrency and scale tools behind it. Next, you will turn that library into fast decisions for unfamiliar interview prompts.
Build a Pattern-Selection Playbook
You now have a complete HLD catalog plus dedicated methods for concurrency, coordination, and capacity. Interview performance depends on selecting a small justified set instead of drawing every component you remember.
This step organises patterns by workload archetype. You will practise naming the bottleneck, guarantee, pattern, and accepted trade-off in one concise answer.
In this step, get ready to:
- Classify common system archetypes.
- Map requirements to pattern choices.
- Build a reusable decision matrix.
Classify the workload
- Compare read-heavy, write-heavy, burst-heavy, real-time, transactional, media, search, geospatial, and analytical systems.
- Identify the dominant request, data shape, latency target, failure cost, and concurrency risk.
- Estimate peak traffic, in-flight work, storage growth, and the first likely bottleneck.
- State the invariant that remains true when requests overlap or retry.
Map patterns to pressure
- Use caching, replicas, indexes, and materialized views for repeated read pressure.
- Use partitioning, batching, streams, and append-friendly storage for sustained write pressure.
- Use queues, priority handling, admission control, and load shedding for bursts.
- Use constraints, version checks, idempotency, per-key ordering, leases, or consensus according to the invariant.
- Use persistent connections, ordered partitions, and presence state for real-time interaction.
- Use scale units, deployment stamps, and tenant bulkheads when growth or noisy neighbours require isolation.
Build the interview matrix
- Create columns for requirements, estimates, bottleneck, invariant, storage, communication, concurrency, scaling, failure, security, and observability.
- Add rows for feeds, chat, payments, media, notifications, search, metrics, file storage, collaborative editing, and ticket booking.
- Select only the patterns that solve a named requirement or pressure.
- Write one rejected alternative and one accepted trade-off for every row.
- Practise presenting an unfamiliar row in two minutes.
Your HLD knowledge is now organised for fast selection and explanation. Next, you will shift from system-wide architecture to C++ object and class design.
Learn the LLD Interview Framework
Your HLD playbook explains services, storage, and distributed trade-offs. LLD zooms into one process and asks how objects collaborate while requirements change.
This step gives you a repeatable LLD interview sequence. You will separate use cases, invariants, responsibilities, interfaces, interactions, and extensions before writing C++.
In this step, get ready to:
- Turn requirements into use cases and invariants.
- Assign behaviour to cohesive classes.
- Present diagrams, C++ interfaces, and extensions.
Clarify behaviour before classes
- Identify actors, primary use cases, and excluded features.
- Write invariants that must remain true after every operation.
- List edge cases, invalid transitions, and concurrency assumptions.
- Ask which requirement is most likely to change.
Build the object model
- Extract candidate entities, value objects, policies, repositories, and services.
- Assign each behaviour to the object with the information needed to enforce it.
- Define ownership, multiplicity, and dependency direction.
- Prefer narrow public interfaces over exposed data and large manager classes.
Present and stress-test the design
- Draw a class diagram for static structure.
- Draw a sequence or state diagram for the critical flow.
- Write only the C++ interfaces needed to make responsibilities concrete.
- Apply one extension request and explain which classes change.
- Review error handling, persistence, testability, ownership, and concurrency before finishing.
You now have a sequence that keeps class design tied to behaviour. Next, you will model C++ objects, relationships, and lifetimes safely.
Model C++ Objects and Relationships
Your interview sequence identifies objects and responsibilities. C++ adds a second question: who owns each object, and how long does every reference remain valid?
This step connects encapsulation, polymorphism, composition, and UML relationships to C++17 lifetime rules. Clear ownership prevents leaks, dangling pointers, and accidental copies.
In this step, get ready to:
- Compare association, aggregation, composition, and inheritance.
- Express ownership with values and smart pointers.
- Avoid slicing, cycles, and unsafe polymorphic deletion.
Choose the relationship
- Association: one object temporarily uses another without owning it.
- Aggregation: a container references parts that can outlive it.
- Composition: the whole exclusively owns parts with the same lifetime.
- Inheritance: a subtype fulfils the base contract and can safely substitute for it.
Express ownership in C++
- Use values when identity and polymorphism are unnecessary.
- Use `std::unique_ptr` for exclusive ownership and explicit moves.
- Use `std::shared_ptr` only for genuine shared ownership.
- Use `std::weak_ptr` for non-owning observation of shared objects and to break cycles.
- Inspect this reference model:
#include <memory>
#include <string>
#include <vector>
class PaymentMethod {
public:
virtual ~PaymentMethod() = default;
virtual bool charge(int cents) = 0;
};
class Card final : public PaymentMethod {
public:
bool charge(int cents) override {
return cents > 0;
}
};
class Checkout {
std::unique_ptr<PaymentMethod> method_;
public:
explicit Checkout(std::unique_ptr<PaymentMethod> method)
: method_(std::move(method)) {}
bool pay(int cents) { return method_->charge(cents); }
};
What does this model prove?
- The abstract base has a virtual destructor because deletion can happen through a base pointer.
- `Checkout` owns one payment method through `std::unique_ptr`.
- Constructor injection makes the dependency replaceable in tests.
Protect object semantics
- Follow the Rule of Zero when RAII members can own every resource.
- Use the Rule of Five only when a class directly manages a resource handle.
- Pass polymorphic objects by reference or pointer to avoid object slicing.
- Make non-owning references shorter-lived than their owners.
- Annotate every relationship in your model with owner, observer, or value.
Your C++ model now makes lifetime and substitution visible. Next, you will use SOLID to keep those classes focused and extensible.
Apply SOLID Principles in C++
Your ownership model keeps objects alive safely. SOLID principles keep their responsibilities and dependency directions safe as features accumulate.
This step uses change pressure as the test. A design is stronger when a new policy changes one focused class instead of a switch statement spread across the codebase.
In this step, get ready to:
- Apply all five SOLID principles.
- Use constructor injection and narrow interfaces.
- Detect coupling and abstraction smells.
Apply SOLID by asking change questions
- SRP: what single reason should make this class change?
- OCP: can a new policy arrive as a new implementation?
- LSP: does every subtype preserve the promises of its base?
- ISP: can each client depend on only the operations it uses?
- DIP: do business rules depend on stable abstractions instead of infrastructure details?
Inject policy through an interface
- Inspect this C++ reference implementation:
#include <memory>
#include <string>
class DiscountPolicy {
public:
virtual ~DiscountPolicy() = default;
virtual int apply(int cents) const = 0;
};
class PercentageDiscount final : public DiscountPolicy {
int percent_;
public:
explicit PercentageDiscount(int percent)
: percent_(percent) {}
int apply(int cents) const override {
return cents - (cents * percent_ / 100);
}
};
class CheckoutService {
std::unique_ptr<DiscountPolicy> policy_;
public:
explicit CheckoutService(std::unique_ptr<DiscountPolicy> policy)
: policy_(std::move(policy)) {}
int total(int cents) const { return policy_->apply(cents); }
};
Why is this design extensible?
`CheckoutService` owns orchestration while `DiscountPolicy` owns calculation. A new discount adds an implementation without editing the checkout service.
The interface stays narrow, and constructor injection exposes the dependency for testing.
Recognise SOLID misuse
- Reject interfaces created only to mirror every concrete class.
- Reject inheritance used only for code reuse when substitution is false.
- Reject repositories, factories, or strategies with one fixed implementation and no change pressure.
- Use Tell, Don’t Ask and the Law of Demeter to keep behaviour near data.
- Apply one new discount requirement and mark the classes that change.
Your classes now absorb change through focused contracts. Next, you will study the creational patterns that control construction and ownership.
Use Creational Patterns in C++
SOLID separates policy from use. Creational patterns decide which concrete object appears and whether construction should expose its steps, families, or copying rules.
This step covers all five GoF creational patterns in C++17. The goal is to recognise construction pressure without wrapping every constructor in a factory.
In this step, get ready to:
- Compare Factory Method and Abstract Factory.
- Use Builder and Prototype for complex creation.
- Evaluate Singleton ownership and testability risks.
Map the five patterns
- Factory Method: let a creator choose one product subtype.
- Abstract Factory: create a compatible family of related products.
- Builder: assemble a valid complex object through named steps.
- Prototype: clone a configured polymorphic object when copying is cheaper than rebuilding.
- Singleton: restrict construction to one process-wide instance when one identity is a real invariant.
Implement a safe factory boundary
- Inspect how the factory returns exclusive ownership:
#include <memory>
#include <stdexcept>
#include <string>
class Notifier {
public:
virtual ~Notifier() = default;
virtual std::string channel() const = 0;
};
class EmailNotifier final : public Notifier {
public:
std::string channel() const override { return "email"; }
};
class SmsNotifier final : public Notifier {
public:
std::string channel() const override { return "sms"; }
};
std::unique_ptr<Notifier> makeNotifier(const std::string& type) {
if (type == "email") return std::make_unique<EmailNotifier>();
if (type == "sms") return std::make_unique<SmsNotifier>();
throw std::invalid_argument("unsupported notifier");
}
What does the factory buy?
The caller depends on `Notifier`, while the factory localises subtype selection. Returning `std::unique_ptr` makes transfer of ownership explicit.
Avoid creation-pattern smells
- Use direct construction when creation is simple and the concrete type is intentional.
- Keep Builder validation inside the final build operation or target type.
- Give Prototype a virtual clone that preserves dynamic type and returns owned memory.
- Prefer dependency injection to hidden Singleton access in business logic.
- Create a decision table for all five patterns with one rejection example each.
You can now separate object construction from object use without hiding ownership. Next, you will compose behaviour through structural patterns.
Use Structural Patterns in C++
Creational patterns control how objects appear. Structural patterns control how existing interfaces and objects combine without producing rigid inheritance trees.
This step covers all seven GoF structural patterns. You will compare patterns that are often confused because their diagrams look similar while their intent differs.
In this step, get ready to:
- Map all seven structural patterns to their intent.
- Compose decorators with C++ ownership.
- Distinguish Adapter, Facade, Proxy, and Bridge.
Map the seven patterns
- Adapter: translate an incompatible interface.
- Bridge: vary an abstraction and implementation independently.
- Composite: treat individual objects and trees uniformly.
- Decorator: wrap an object to add stackable behaviour.
- Facade: expose a simpler entry point to a subsystem.
- Flyweight: share immutable intrinsic state across many logical objects.
- Proxy: control access while preserving the subject interface.
Compose a decorator
- Inspect this ownership-safe wrapper:
#include <memory>
#include <string>
class Message {
public:
virtual ~Message() = default;
virtual std::string render() const = 0;
};
class PlainMessage final : public Message {
public:
std::string render() const override { return "hello"; }
};
class EncryptedMessage final : public Message {
std::unique_ptr<Message> inner_;
public:
explicit EncryptedMessage(std::unique_ptr<Message> inner)
: inner_(std::move(inner)) {}
std::string render() const override {
return "encrypted(" + inner_->render() + ")";
}
};
Why does composition matter?
The decorator implements the same interface and exclusively owns the wrapped component. Another decorator can wrap it without changing either class.
Separate similar intents
- Use Adapter when the client and existing object speak different interfaces.
- Use Facade when the subsystem is compatible but too complex for common callers.
- Use Proxy when access, lifecycle, location, or policy must be controlled.
- Use Bridge when two dimensions of variation would otherwise create a subclass explosion.
- Draw one comparison diagram for Adapter, Decorator, Facade, and Proxy.
You can now combine objects without confusing interface translation, simplification, access control, and added behaviour. Next, you will model changing algorithms, state, and collaboration.
Use Behavioral Patterns in C++
Structural patterns organise object composition. Behavioral patterns organise algorithms, requests, state transitions, notifications, and collaboration.
This step covers all eleven GoF behavioral patterns. You will focus on intent and change pressure because several patterns can implement similar diagrams.
In this step, get ready to:
- Map all eleven behavioral patterns.
- Compare Strategy, State, Command, and Template Method.
- Choose collaboration patterns without creating hidden coupling.
Map the eleven patterns
- Chain of Responsibility: pass a request through ordered handlers.
- Command: represent an action as an object.
- Interpreter: evaluate a small language from a grammar.
- Iterator: traverse a collection without exposing representation.
- Mediator: centralise collaboration among peers.
- Memento: capture restorable state without exposing internals.
- Observer: notify subscribers of changes.
- State: change behaviour as internal state changes.
- Strategy: select a replaceable algorithm.
- Template Method: fix an algorithm skeleton while subclasses fill steps.
- Visitor: add operations across a stable type hierarchy.
Implement a strategy boundary
- Inspect how the context receives a replaceable algorithm:
#include <memory>
#include <vector>
class AllocationStrategy {
public:
virtual ~AllocationStrategy() = default;
virtual int choose(const std::vector<bool>& free) const = 0;
};
class FirstFree final : public AllocationStrategy {
public:
int choose(const std::vector<bool>& free) const override {
for (int i = 0; i < static_cast<int>(free.size()); ++i)
if (free[i]) return i;
return -1;
}
};
class Allocator {
std::unique_ptr<AllocationStrategy> strategy_;
public:
explicit Allocator(std::unique_ptr<AllocationStrategy> strategy)
: strategy_(std::move(strategy)) {}
int allocate(const std::vector<bool>& free) const {
return strategy_->choose(free);
}
};
How do the four patterns differ?
Strategy is selected from outside the context. State objects usually change as the context transitions, while Command captures an invocation and Template Method relies on inheritance.
Control collaboration costs
- Give Observer subscriptions a clear lifetime and unsubscription path.
- Use Mediator when peer-to-peer references create a dense dependency graph.
- Use Chain only when handler order and stopping rules are explicit.
- Use Visitor when types are stable and operations change more often.
- Create a pattern matrix with intent, trigger, cost, and rejection signal for all eleven patterns.
You can now select all 23 GoF patterns by intent instead of diagram shape. Next, you will model domain rules, state transitions, and application boundaries.
Model Domains and Application Layers
Patterns provide reusable collaboration shapes, but they do not decide where business rules belong. Domain modelling keeps invariants close to the objects that understand them.
This step separates entities, value objects, services, repositories, and transport models. You will also model lifecycle rules as explicit states instead of scattered booleans.
In this step, get ready to:
- Distinguish entities and value objects.
- Separate domain, application, and infrastructure responsibilities.
- Model invariants and state transitions.
Model domain identity
- Use entities when identity persists while attributes change.
- Use immutable value objects when equality comes from contained values.
- Keep invariants inside constructors and behaviour that can create a valid state.
- Define aggregate boundaries around invariants that must change atomically.
Separate application layers
- Use controllers or adapters to translate transport input.
- Use application services to orchestrate one use case.
- Use domain services only for rules that do not belong to one entity or value object.
- Use repository interfaces to express aggregate persistence without leaking database queries into the domain.
- Use DTOs and mappers at boundaries instead of exposing domain objects directly.
Model state explicitly
- Write legal transitions before adding methods.
- Use an enum and guarded transition function for small stable state machines.
- Use the State pattern when each state owns substantial behaviour.
- Record transition reasons and events when auditability matters.
- Draw one aggregate, its repository boundary, and its state machine.
Your design now gives business rules a clear home and lifecycle. Next, you will use UML to communicate both static structure and runtime interaction.
Draw UML and Interaction Diagrams
Your domain model now has boundaries and legal states. Diagrams make those decisions visible before implementation details hide them.
This step uses each UML view for a different question. You will keep diagrams selective enough to discuss and precise enough to expose contradictions.
In this step, get ready to:
- Draw use-case and class diagrams.
- Trace interactions with sequence and activity diagrams.
- Model lifecycle rules with state diagrams.
Show static structure
- Use a use-case diagram to establish actors and system scope.
- Use a class diagram to show responsibilities, public operations, relationships, and multiplicity.
- Mark composition, aggregation, association, inheritance, and dependency only when they change the design discussion.
- Show interface dependency direction instead of listing every private field.
Show runtime interaction
- Use a sequence diagram to show call order, object lifelines, returns, alternatives, and asynchronous messages.
- Use an activity diagram when branching workflow is more important than object identity.
- Include validation, failure, retry, and compensation paths in the critical interaction.
- Keep one sequence diagram focused on one use case.
Show lifecycle behaviour
- Use a state diagram for legal states, events, guards, actions, and terminal outcomes.
- Cross-check every public state-changing method against the state diagram.
- Cross-check every sequence message against an operation or responsibility in the class model.
- Create a class, sequence, and state diagram for the same use case.
Your static and dynamic views now tell the same story. Next, you will make that story safe when several threads act at once.
Design Thread-Safe C++ Classes
Your diagrams are correct for one caller. Concurrent requests can still interleave between a check and an update, which breaks reservations, balances, and inventory.
This step treats thread safety as part of each class contract. You will protect invariants with RAII locking, narrow critical sections, and explicit waiting rules.
In this step, get ready to:
- Find races and choose synchronisation primitives.
- Design lock scope and ordering.
- Use atomics and condition variables correctly.
Protect a compound invariant
- Inspect this C++17 inventory operation:
#include <mutex>
class Inventory {
int available_;
mutable std::mutex mutex_;
public:
explicit Inventory(int available) : available_(available) {}
bool reserve(int quantity) {
std::lock_guard<std::mutex> lock(mutex_);
if (quantity <= 0 || quantity > available_) return false;
available_ -= quantity;
return true;
}
int available() const {
std::lock_guard<std::mutex> lock(mutex_);
return available_;
}
};
Why does the lock cover both operations?
The same mutex protects the check and decrement, so another thread cannot consume inventory between them. `std::lock_guard` releases the mutex when the scope exits, including early returns.
Choose the primitive
- Use a mutex when several fields or operations form one invariant.
- Use an atomic for independent counters or flags that fit one atomic operation.
- Use `std::condition_variable` with `std::unique_lock` and a predicate when a thread must wait for shared state.
- Use immutability and thread confinement to remove sharing where possible.
- Avoid assuming `std::shared_ptr` makes the pointed-to object thread-safe.
Prevent deadlock and starvation
- Keep critical sections short and avoid slow I/O while holding a lock.
- Acquire multiple locks in one global order.
- Use `std::scoped_lock` when one operation must acquire several mutexes together.
- Define fairness and timeout behaviour for contended operations.
- Annotate shared fields and lock ownership on your class diagram.
Your classes now preserve invariants across interleaving calls. Next, you will make them easy to test and safe to extend.
Make C++ Designs Testable and Extensible
Thread-safe code can still be difficult to verify if dependencies, time, and randomness are hidden. Testability exposes important collaborators as controlled seams.
This step uses interfaces, fakes, and requirement changes to test design quality. You will also distinguish useful seams from abstraction added only for ceremony.
In this step, get ready to:
- Inject time, persistence, and external dependencies.
- Choose mocks, stubs, fakes, and state-based tests.
- Run extension and refactoring checks.
Create useful test seams
- Inject clocks when expiry, scheduling, or retries depend on time.
- Inject repository interfaces when tests need deterministic persistence.
- Inject gateways for payments, notifications, files, or network calls.
- Pass deterministic random sources when allocation or backoff uses randomness.
Choose the test double
- Stub: return prepared answers.
- Fake: provide a lightweight working implementation, such as an in-memory repository.
- Mock: verify an important interaction contract.
- Prefer state-based assertions when outcomes reveal correctness without coupling tests to call order.
- Test invalid transitions, duplicate requests, boundary values, and concurrent conflicts.
Stress-test extensibility
- Add one new policy and count the existing classes that change.
- Replace one infrastructure dependency and check whether domain code changes.
- Remove one speculative interface that has no alternate implementation or test need.
- Refactor internal structure while keeping behaviour-focused tests unchanged.
- Create an LLD review rubric for correctness, clarity, ownership, concurrency, and extensibility.
Your design can now be verified and evolved without rewriting its core. Next, you will apply the full method to a parking lot.
Design a Parking Lot in C++
Your C++ LLD toolkit is complete. Parking allocation now tests object boundaries, pluggable policies, pricing, and concurrent entry requests in one familiar domain.
You will model vehicles, spots, floors, tickets, gates, and payments. The design must add a new vehicle or pricing policy without changing core orchestration.
In this step, get ready to:
- Define parking invariants and class responsibilities.
- Apply Strategy and Factory patterns.
- Protect allocation from concurrent claims.
Model the domain
- Model `Vehicle`, `ParkingSpot`, `ParkingFloor`, `Ticket`, `Gate`, `Payment`, and `ParkingLot`.
- Represent vehicle and spot categories with scoped enums or focused value types.
- Keep spot occupancy and ticket lifecycle transitions guarded by methods.
- Make the lot compose floors and floors compose spots.
Separate policies
- Define `SpotAllocationStrategy` for nearest, first-free, or electric-compatible placement.
- Define `PricingStrategy` for hourly, flat, weekend, or vehicle-specific pricing.
- Use a Factory only when the vehicle or payment subtype must be selected from runtime input.
- Inject repositories and clocks into the service that opens and closes tickets.
Protect the critical flow
- Make spot selection and reservation one atomic operation.
- Use a reservation token or guarded spot method to reject duplicate claims.
- Make payment completion idempotent before marking a ticket closed.
- Draw a class diagram and an entry sequence with failure paths.
- Extend the model with reservations and electric charging.
Your first full LLD now combines policies, ownership, persistence, and concurrency. Next, you will model movement and scheduling in an elevator system.
Design an Elevator System in C++
Parking allocation selected a static resource. Elevators add movement, queued requests, safety states, and scheduling across several machines.
You will model internal and external requests separately, then coordinate cars without placing every decision inside one controller.
In this step, get ready to:
- Model elevator state and requests.
- Separate dispatch and car scheduling policies.
- Handle concurrency, safety, and maintenance.
Model cars and requests
- Model `ElevatorCar`, `Door`, `Motor`, `FloorPanel`, `CarPanel`, `Request`, and `ElevatorSystem`.
- Represent direction, motion, door, emergency, and maintenance states explicitly.
- Use Command objects for hall calls and in-car destination requests when queuing and cancellation matter.
- Keep hardware adapters behind narrow interfaces.
Separate scheduling policies
- Use a dispatch Strategy to choose a car for a hall request.
- Use a car scheduling Strategy to order stops for one elevator.
- Compare nearest-car, directional sweep, and load-aware decisions.
- Give the controller current state snapshots instead of unrestricted mutable car access.
Protect movement and safety
- Serialise state transitions for each car.
- Reject movement while doors are open or a safety state is active.
- Define timeout and recovery behaviour for stuck doors and unavailable floors.
- Draw a state diagram and request sequence.
- Extend the design with priority service and destination dispatch.
Your elevator design separates physical state from scheduling policy. Next, you will apply explicit state transitions to a vending machine.
Design a Vending Machine in C++
Elevator behaviour changed with movement and safety state. A vending machine makes the State pattern smaller and exposes payment, inventory, cancellation, and refund invariants clearly.
You will model legal transitions from selection through payment and dispensing. Invalid actions must fail without corrupting credit or stock.
In this step, get ready to:
- Model inventory, credit, and machine states.
- Apply State and Strategy patterns.
- Handle refunds and concurrent inventory updates.
Define the state machine
- Model idle, selected, paid, dispensing, sold-out, and maintenance states.
- Define events for selecting, inserting funds, cancelling, dispensing, and refilling.
- Keep credit, selected slot, and reservations consistent across every transition.
- Use the State pattern when each state rejects or handles several operations differently.
Separate policies and devices
- Use a change-calculation Strategy when supported denominations or optimisation rules vary.
- Hide coin, card, display, and dispenser hardware behind interfaces.
- Use value objects for money instead of floating-point amounts.
- Use an inventory repository or composed slot collection as the source of stock truth.
Protect money and stock
- Reserve stock before accepting a final payment.
- Make cancellation release reservations and return recorded credit once.
- Handle dispenser failure with a recoverable refund state.
- Draw a state diagram and a successful-purchase sequence.
- Extend the model with digital payments and remote inventory monitoring.
Your vending machine preserves money and inventory through explicit transitions. Next, you will design a multi-channel notification system.
Design a Notification System in C++
The vending machine coordinated one device and one transaction. Notifications introduce multiple channels, providers, templates, preferences, retries, and asynchronous delivery.
You will separate message intent from channel transport. Adding a provider or channel should not require rewriting the orchestration service.
In this step, get ready to:
- Model messages, channels, providers, and preferences.
- Apply Factory, Strategy, and Observer boundaries.
- Design retry, fallback, and idempotency behaviour.
Separate message intent and transport
- Model `Notification`, `Recipient`, `Template`, `Preference`, `DeliveryAttempt`, and `Channel`.
- Use a channel Strategy for email, SMS, push, and in-app delivery.
- Use a provider adapter behind each external vendor interface.
- Use a Factory when channel or provider selection comes from runtime policy.
Apply recipient policy
- Resolve preferences before creating delivery attempts.
- Separate transactional, security, and marketing policy.
- Render templates with typed data and locale-aware values.
- Use Observer or domain events to request notifications without coupling business objects to transports.
Make delivery reliable
- Assign an idempotency key to each logical channel delivery.
- Retry transient provider failures with bounded backoff.
- Use fallback providers only when duplicate-delivery and policy risks are controlled.
- Store attempt state and provider responses for audit and support.
- Draw the C++ class model and asynchronous delivery sequence.
Your notification design isolates channels, vendors, and delivery policy. Next, you will model an exact financial ledger for shared expenses.
Design an Expense-Sharing System in C++
Notifications tolerate retries and eventual delivery. Expense sharing raises stricter requirements for exact splits, rounding, immutable history, and concurrent ledger updates.
You will model equal, exact, and percentage splits as policies. Balances will be derived from ledger entries rather than overwritten as unexplained totals.
In this step, get ready to:
- Model expenses, splits, and ledger entries.
- Apply Strategy for split calculation.
- Protect rounding and concurrent updates.
Model financial value
- Model `Money` as an integer minor-unit amount plus currency.
- Model `User`, `Group`, `Expense`, `Split`, `LedgerEntry`, and `Settlement`.
- Keep expenses and ledger entries immutable after posting.
- Reject mixed currencies until an explicit exchange policy exists.
Calculate splits through policy
- Use `SplitStrategy` implementations for equal, exact, percentage, and share-based splits.
- Validate exact totals and percentage totals before posting.
- Assign rounding remainder deterministically.
- Keep debt simplification separate from source-of-truth ledger entries.
Protect the ledger
- Post all entries for one expense atomically.
- Use an idempotency key for retried expense creation.
- Use optimistic version checks or a per-group lock for concurrent updates.
- Draw the class model and expense-posting sequence.
- Extend the design with settlements and multiple currencies.
Your ledger model preserves exact history and flexible split policy. Next, you will connect C++ class design to a scalable rate limiter.
Design a Rate Limiter in C++
The expense system enforced exact ledger invariants. A rate limiter connects LLD to HLD by combining replaceable algorithms, clocks, concurrent counters, storage boundaries, and distributed consistency.
You will design local Token Bucket and Sliding Window policies first. Then you will identify which interfaces change when state moves to a shared store.
In this step, get ready to:
- Model keys, limits, decisions, and clocks.
- Implement replaceable limiter strategies.
- Scale local state to distributed state.
Define the C++ contract
- Model `RateLimitKey`, `Limit`, `Decision`, `Clock`, `RateLimiter`, and `CounterStore`.
- Return allowed status, remaining capacity, and retry timing in a decision value.
- Inject a clock so time-dependent tests remain deterministic.
- Use Strategy implementations for Token Bucket, Fixed Window, and Sliding Window.
Protect local state
- Keep refill and token consumption in one critical section.
- Use one lock per key or a striped-lock map when a global mutex becomes a bottleneck.
- Evict inactive keys without racing with active requests.
- Use integer units and monotonic time to avoid rounding and wall-clock jumps.
Move to distributed enforcement
- Place shared counters behind `CounterStore` instead of changing the algorithm-facing service.
- Require atomic server-side updates for check-and-consume operations.
- Partition counters by a stable key and define behaviour during store failure.
- Decide whether local fallback may exceed a global limit.
- Draw local and distributed class and sequence diagrams.
You have connected C++ object design to distributed enforcement and failure policy. Next, you will return to HLD and combine the full toolkit in realistic systems.
Design a Food Delivery Platform
Your HLD and C++ LLD toolkits are complete. This guided case combines them into a food-delivery system with geospatial reads, transactional orders, asynchronous workflows, and real-time tracking.
You will scope the product, estimate its workload, draw the architecture, and defend the critical order flow. Every component must answer a named requirement or failure mode.
In this step, get ready to:
- Scope and estimate the food-delivery workload.
- Design geospatial browse and real-time tracking paths.
- Coordinate orders, payments, drivers, and notifications.
Scope the system
- Define restaurant discovery, menu browsing, order placement, payment, driver assignment, tracking, and notifications as the core scope.
- Estimate peak browse, order, location-update, storage, and bandwidth demand from explicit assumptions.
- Mark order and payment state as transactional.
- Mark location and tracking updates as high-write and freshness-sensitive.
Design browse and tracking paths
- Use a geospatial index to find restaurants and drivers near a location.
- Cache menus and restaurant summaries with explicit invalidation.
- Ingest driver pings through a partitioned write path.
- Stream tracking updates to clients through persistent connections.
- Serve restaurant media through object storage and a CDN.
Coordinate the order lifecycle
- Create orders and payment attempts with idempotency keys.
- Use a saga for payment, restaurant acceptance, driver assignment, and compensation.
- Publish committed order events through a transactional outbox.
- Fan events out to restaurant, delivery, and notification consumers.
- Draw the architecture and label scaling, consistency, retry, and observability decisions.
You have combined geospatial, transactional, event-driven, and real-time patterns in one design. Next, you will apply the same discipline independently to chat.
Design a WhatsApp Chat System
Design a WhatsApp-Style Chat System
Your food-delivery design combined several patterns with guidance. Chat now tests whether you can select connection, storage, ordering, fan-out, and offline-delivery patterns independently.
You will design one-to-one and group messaging, presence, receipts, and media delivery. The result must remain correct when clients disconnect, retry, or reconnect to another server.
In this step, get ready to:
- Estimate connections, message traffic, and storage.
- Design ordered online and offline delivery.
- Handle group fan-out, presence, receipts, and media.
Define the chat contract
- Scope one-to-one messages, group messages, presence, delivery receipts, and media.
- Estimate concurrent connections separately from messages per second.
- Define ordering per conversation instead of promising global order.
- Assign client-generated message IDs for retry and deduplication.
Design message delivery
- Use WebSocket gateways for persistent bidirectional connections.
- Store user-to-gateway mappings in a connection registry.
- Partition durable messages by conversation or recipient according to the ordering model.
- Use store-and-forward delivery for offline recipients.
- Advance sent, delivered, and read receipts through idempotent events.
Handle groups, presence, and media
- Choose group fan-out at write, delivery, or read according to membership and group size.
- Use heartbeats and expiry to model approximate online presence.
- Upload media directly to object storage through signed URLs.
- Serve media through a CDN while storing only metadata in messages.
- Draw the architecture with ordering, retry, offline, and hot-group paths.
You have designed a stateful real-time system with independent delivery guarantees. Next, you will test fan-out choices directly in a social feed.
Design a Facebook-Style News Feed
Your chat design handled ordered delivery to known recipients. A social feed adds ranking, celebrity fan-out, privacy changes, and a read path that must stay fast under unpredictable traffic.
You will design posting, following, feed generation, reactions, and media delivery. The central trade-off is where fan-out work happens and how the system treats users with millions of followers.
In this step, get ready to:
- Estimate posts, feed reads, fan-out writes, and storage.
- Compare push, pull, and hybrid feed generation.
- Protect privacy, ranking freshness, and hot users.
Define the feed contract
- Scope posting, following, home-feed reads, reactions, comments, and media.
- Estimate daily active users, posts per second, feed reads per second, average follower count, and celebrity fan-out.
- Define freshness, availability, privacy, and ranking requirements.
- Use opaque cursors for stable pagination through changing feeds.
Choose a fan-out model
- Use fan-out on write for ordinary accounts when fast feed reads justify duplicated timeline entries.
- Use fan-out on read for celebrity accounts to avoid enormous write bursts.
- Merge precomputed and pulled posts through a hybrid feed service.
- Partition inbox timelines by recipient and post storage by author or post ID.
- Process follow changes and feed updates through idempotent events.
Rank and protect the feed
- Fetch candidate posts before applying ranking features and policy filters.
- Apply privacy and block rules at serving time for revocations that must take effect quickly.
- Cache feed pages with short lifetimes and invalidate sensitive entries explicitly.
- Rate-limit posting, reactions, and abusive scraping independently.
- Draw the write path, read path, celebrity path, and privacy-change path.
Your feed design balances precomputation, ranking, and hot-user protection. Next, you will design video ingestion and global playback for a Netflix-style platform.
Design a Netflix-Style Streaming Platform
Your social feed moved small posts and media references through a ranked read path. Video streaming adds large immutable files, expensive transcoding, adaptive playback, and global bandwidth pressure.
You will separate the upload control plane from the playback data plane. The design must absorb popular releases without sending every video byte through application servers.
In this step, get ready to:
- Estimate catalogue storage, encoding work, and playback bandwidth.
- Design upload, transcoding, and publication workflows.
- Serve adaptive video through global caches.
Separate control and data planes
- Scope catalogue browse, title metadata, playback, progress, and creator ingestion.
- Estimate concurrent streams, average bit rate, peak bandwidth, source storage, and encoded storage.
- Keep authentication, entitlement, catalogue, and playback-session decisions in the control plane.
- Keep video segments on object storage and CDN paths in the data plane.
Build the media pipeline
- Upload source files directly to object storage through short-lived signed requests.
- Publish an ingestion event after the source object is durably recorded.
- Transcode each title into several resolutions and bit rates through parallel workers.
- Generate manifests, thumbnails, subtitles, and integrity metadata.
- Mark a title playable only after required renditions and metadata succeed.
Serve reliable playback
- Return a manifest that lets the client adapt quality to current bandwidth.
- Cache popular segments near viewers and shield origin storage from misses.
- Use range requests and immutable segment names for efficient retries and caching.
- Store playback progress idempotently and tolerate delayed cross-device updates.
- Draw ingestion, playback, cache-miss, and regional-failure paths.
Your streaming platform separates compute-heavy ingestion from bandwidth-heavy playback. Next, you will combine geospatial matching, live location, and trip workflows in an Uber-style design.
Design an Uber-Style Ride-Hailing System
Your streaming design handled global delivery of immutable media. Ride hailing combines volatile driver locations, nearby matching, reservation races, pricing, payments, and a long-running trip state machine.
This final capstone asks you to connect every major HLD and LLD skill. You will defend the architecture, the trip workflow, the in-process domain model, and the failure policy.
In this step, get ready to:
- Estimate location, search, trip, and tracking workloads.
- Design nearby-driver matching and reservation.
- Model the trip lifecycle and failure recovery.
Define the ride contract
- Scope ride requests, estimates, driver matching, live tracking, trip state, payment, and notifications.
- Estimate online drivers, location updates per second, ride requests per second, search radius, and retention.
- Define pickup latency, match success, location freshness, availability, and payment correctness goals.
- Separate approximate location data from authoritative trip and payment state.
Match and reserve a driver
- Ingest driver pings into a partitioned geospatial index with expiry.
- Query nearby available drivers by expanding cells or search radius.
- Rank candidates by pickup time, vehicle fit, acceptance likelihood, and fairness.
- Reserve one driver with a conditional state transition or short lease.
- Retry matching with bounded expansion when candidates reject or leases expire.
Coordinate the trip lifecycle
- Model requested, matching, assigned, arriving, in-trip, completed, and cancelled states.
- Use a saga for driver reservation, trip creation, payment authorisation, and compensation.
- Publish committed trip changes through an outbox for tracking and notifications.
- Make client retries idempotent and reject invalid or stale state transitions.
- Draw the HLD architecture, C++ trip aggregate, match sequence, and regional-failure path.
Secret mission
Scale Chat to 2 Billion Users
Scale your WhatsApp-style system to 2 billion registered users. Recalculate the workload, evolve the design across regions, and defend the new consistency and failover trade-offs.
Clean Up Your Resources
Clean Up Your Resources
This project creates diagrams and notes in Eraser. It does not provision servers, databases, or other running infrastructure.
Decide whether to keep the study file, pause your work, or archive it. Eraser's current Free plan supports up to three files, so archiving can free your active workspace for another project.
Resources you used:
- The `System Design Practice` Eraser file containing HLD diagrams, UML models, C++ notes, case studies, and the interview playbook.
Keep everything running
No action is needed. Choose this if you are still reviewing diagrams, refining C++ models, or preparing for interviews.
- Keep the `System Design Practice` file in your Eraser dashboard.
- Rename or organise diagrams so each case study remains easy to find.
- Export important diagrams if you want an offline copy.
Pause - I'll come back to this later
There are no running processes to stop. Choose this if you want to leave the file unchanged and return later.
- Close the Eraser browser tab.
- Record the next case or concept you want to review.
- Return to the same file when you resume.
Delete - I don't want to use this again
Removing the study file is destructive. Export anything you want to keep before starting Eraser's archive and deletion flow.
- Open the Eraser dashboard.
- Find the `System Design Practice` file.
- Click the three-dot button at the right end of the file row.
- Select **Archive File**.
- Open the **Archived** page on the dashboard to confirm the file moved out of the active list.
- Follow the official Eraser file-deletion guide if you want to permanently remove the archived file.
Nice Work!
Nice Work!
Nice work! You built a complete HLD and C++ LLD interview toolkit, including cloud patterns, distributed coordination, capacity planning, object models, and large-scale system cases.
You've learned how to:
- Use an HLD interview framework to estimate workloads, preserve invariants, identify bottlenecks, and defend trade-offs.
- Select from 44 cloud architecture patterns across edge, service, data, messaging, workflow, reliability, security, and operations problems.
- Design C++ object models with SOLID principles, UML, all 23 GoF patterns, explicit ownership, thread safety, and test seams.
- Complete the Secret Mission by scaling a WhatsApp-style design to 2 billion registered users with regional routing, partitioning, backpressure, and failover.
Ready to quiz yourself?