5 min read

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 INSERT and UPDATE statements 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.

Related Guides:

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.

← PreviousDeep Dive into Detailed Design/Component DesignNext →Deep Dive into System Integration Design