High-Level Design (HLD): The Big Picture Before You Write Code
Imagine trying to build an airport by hiring bricklayers before deciding where the runways go. That is what coding without High-Level Design feels like. In this guide, we break down HLD from the ground up: what it actually looks like in real engineering teams, how to draw the boxes without getting stuck in trivia, and how to scale from 10 users to 10 million.
👉 Next Step: Once your big-picture boxes make sense, see how to turn them into clean classes and interfaces in our companion guide: Low-Level Design (LLD).
What Exactly is High-Level Design?
When engineers say “High-Level Design” (or HLD), they mean one simple thing: the 30,000-foot view of your entire system.
At this stage, you don't care what your database columns are called. You don't care which design patterns your classes use. You care about the big moving pieces:
- Where do user requests enter our system?
- Which service processes what?
- Where does the data actually live?
- What happens when a million people click “Buy” at the same second?
- If one database or server crashes, does the whole site go dark, or does it stay up?
Think of HLD as the floor plan of a house. It shows where the bedrooms, kitchen, and load-bearing walls go. It does not show where you plug in your toaster — that comes later in Low-Level Design.
The Anatomy of a Modern Web System
Almost every major scalable system — from Instagram to Shopify — shares the same fundamental building blocks. Here is the journey of a request:
flowchart TD
User(["User on Phone / Laptop"])
DNS["1. DNS Lookup\n(Finds nearest server IP)"]
CDN["2. CDN Edge Cache\n(Returns images/CSS instantly)"]
LB["3. Load Balancer\n(Distributes traffic evenly)"]
subgraph AppServers["Application Tier (Stateless Servers)"]
S1["Server 1"]
S2["Server 2"]
S3["Server 3"]
end
Cache[("4. In-Memory Cache (Redis)\n(Holds hot data for fast reads)")]
PrimaryDB[("5. Primary Database\n(Handles all writes)")]
ReplicaDB[("6. Read Replicas\n(Handles all read queries)")]
User --> DNS
DNS --> CDN
CDN -.->|"Cache Miss: Fetch from Origin"| LB
User -->|"Dynamic API Request"| LB
LB --> S1 & S2 & S3
S1 <--> Cache
S2 <--> Cache
S3 <--> Cache
S1 -->|"Writes"| PrimaryDB
PrimaryDB -.->|"Async Replication"| ReplicaDB
S2 -->|"Reads"| ReplicaDB
Let's Walk Through the Layers
- The Front Door (DNS & CDN): When a user types your URL, DNS resolves the domain name to an IP. If they are loading images or videos, they hit a Content Delivery Network (CDN) like Cloudflare. The request never touches your servers, saving massive bandwidth.
- The Traffic Cop (Load Balancer): Your servers will crash if one gets 100,000 requests while another sits idle. The load balancer (NGINX, AWS ALB) distributes traffic across multiple identical application servers.
- The Stateless Compute Tier: Your backend code (Node.js, Go, Java, Python). Crucially, these servers should be stateless — they don't store user sessions in local server memory. That way, if Server 2 dies, Server 3 can handle the very next request without logging the user out.
- The Speed Booster (Cache): Querying a relational database from disk takes 10 to 50 milliseconds. Reading from an in-memory Redis cache takes under 1 millisecond. Putting hot data (user profiles, popular tweets) in cache protects your database from melting.
-
The Database Split (Primary vs. Replicas): In most apps, users read data 95% of the time and write data only 5% of the time. We send all
INSERTandUPDATEstatements to one Primary database, and let multiple Read Replicas handle the heavy read traffic.
Real-World Example: Designing a Ride-Hailing Matcher (Uber-style)
To see how an architect thinks through an HLD problem, imagine building the dispatch service when a rider asks for a car:
sequenceDiagram
autonumber
actor Rider as Rider App
participant LB as API Gateway
participant Dispatch as Dispatch Service
participant Geo as Geospatial Index (Redis H3)
participant Queue as Kafka Message Bus
actor Driver as Driver App
Rider->>LB: 1. Request Ride (pickup lat/lng)
activate LB
LB->>Dispatch: Forward Request
activate Dispatch
Dispatch->>Geo: 2. Who are the closest available drivers?
activate Geo
Geo-->>Dispatch: Returns [Driver #42, Driver #87]
deactivate Geo
Dispatch->>Queue: 3. Publish "RideOffered" event for Driver #42
activate Queue
Queue-->>Driver: 4. Push Notification: "New Ride Nearby!"
deactivate Queue
Driver->>Queue: 5. Driver clicks "Accept Ride"
activate Queue
Queue-->>Dispatch: 6. Consume "RideAccepted"
deactivate Queue
Dispatch-->>LB: 7. Driver Assigned: #42 on the way
deactivate Dispatch
LB-->>Rider: 8. Show Driver on Map!
deactivate LB
Notice what makes this design resilient:
- The dispatch service doesn't hold an open, blocking connection to the driver's phone. Phones lose signal in tunnels all the time!
- Instead, it uses a message queue (like Kafka or RabbitMQ). If the driver doesn't respond within 15 seconds, the dispatch service simply pops the next closest driver from the list.
The Three Numbers Every Senior Engineer Calculates
Junior engineers jump straight into choosing databases. Senior engineers start with the back-of-the-envelope math. Before you design anything, answer these three questions:
1. What is the Read-to-Write Ratio?
Are you building Twitter (1,000 reads for every 1 tweet posted) or an IoT sensor logger (1,000,000 writes per second, read once a week for reporting)? This single answer determines whether you need aggressive read-caching or a write-optimized database like Cassandra.
2. What is our Peak QPS (Queries Per Second)?
10 million daily active users sounds like a lot, but what does that mean per second?
10,000,000 requests / 86,400 seconds in a day ≈ 115 requests/second average.
Peak traffic is usually 2x to 5x average ≈ 600 requests/second.
A single standard cloud server can comfortably handle 600 requests per second! Knowing this keeps you from over-engineering a 50-microservice cluster for traffic a single well-tuned box could handle.
3. What happens when a hard drive or datacenter dies?
If your database server loses power right now, how many minutes of data can you afford to lose (Recovery Point Objective), and how many minutes until the backup takes over (Recovery Time Objective)?
Common Beginner Mistakes in HLD
- The “Single Point of Failure” Trap: You have 5 web servers, but only one database server with no automated failover. If that one database machine restarts, your entire business is offline.
- Premature Microservices: Splitting a 3-person team's app into 15 microservices. Now you spend 80% of your day fixing network serialization bugs, Kubernetes configs, and distributed tracing instead of building product features.
- Forgetting Caches Go Stale: Storing user balances in Redis without a crystal-clear invalidation plan. Suddenly, a customer spends money, refreshes their screen, and sees their old balance because the cache didn't clear.
- To turn these high-level boxes into concrete classes and code, read Low-Level Design (LLD).
- To learn how services talk over the network without breaking, read System Integration & API Design.
- To dive into database replication, sharding, and indexes, explore Database Design.
Final Thoughts
High-Level Design is not about drawing pretty boxes on a whiteboard to look smart. It is about de-risking your software before you build it. When you understand your traffic, split your reads from your writes, and design assuming servers will fail, you build systems that let you sleep peacefully through the night.