Why They Feel Separate but Are Actually OneSystem
HLD vs LLD-most beginners learn these as separate topics,along with API Design,in three separate courses. They finish all three and still cannot connect them. They can describe a CDN, draw an ER diagram, and write a REST endpoint — but they cannot explain how those three interlock when building a real system. This guide builds that bridge.
The simplest mental model: HLD draws the map, LLD designs the buildings, API Design specifies the roads between them.
HLD (High-Level Design): Which services exist? Where does data live? How does a request flow from user to response? Think in systems, boxes, and arrows.
LLD (Low-Level Design): Inside each service, how is the database structured? What tables, columns, types, and indexes? What classes and methods?
API Design: How do services talk to each other and to clients? What contracts govern communication? Endpoints, request/response schemas, authentication.
They are not sequential phases. Changes in one force changes in all three. Add a new feature (HLD decision) and you need a new table (LLD change) and a new endpoint (API change). Delete a database column (LLD change) and you must update every API that returned that field (API change) and re-assess whether the service that used it still makes architectural sense (HLD change).
Full Twitter-Like System — Designed Across All Three Layers
HLD Layer — The Map
Requirement: Users can register, post tweets, follow other users, and see a home timeline of tweets from people they follow. Support 100M monthly users, 1M tweets per day.
HLD Components:
Client (Browser / Mobile App)
↓
CDN (static assets: images, JS, CSS)
↓
API Gateway (rate limiting, auth verification, routing)
↓
┌─────────────────────────────────────────────────────┐
│ Load Balancer │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ User Service │ │ Tweet Service │ │
│ │ (auth, profile)│ │ (create, read tweets) │ │
│ └───────┬─────────┘ └─────────┬────────────────┘ │
└──────────│───────────────────── │───────────────────┘
│ │
PostgreSQL PostgreSQL
(user data) (tweet data)
↘ ↙
Redis Cache
(home timelines,
user sessions)
│
Message Queue (Kafka)
(fanout: distribute new
tweets to followers'
timeline caches)LLD Layer — The Buildings
Now we go inside each service and design the database schema:
-- User Service database
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
bio TEXT,
avatar_url TEXT,
follower_count INT DEFAULT 0, -- denormalized for fast reads
following_count INT DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE follows (
follower_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
following_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (follower_id, following_id)
);
-- Tweet Service database
CREATE TABLE tweets (
id BIGSERIAL PRIMARY KEY,
author_id BIGINT NOT NULL, -- no FK (different DB/service!)
content VARCHAR(280) NOT NULL,
like_count INT DEFAULT 0,
reply_count INT DEFAULT 0,
reply_to_id BIGINT REFERENCES tweets(id), -- for threads
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_tweets_author_created ON tweets(author_id, created_at DESC);
CREATE INDEX idx_tweets_created ON tweets(created_at DESC);API Design Layer — The Roads
# Contracts between clients and services:
# --- User Service Endpoints ---
POST /v1/auth/register {username, email, password} → 201 {id, username, token}
POST /v1/auth/login {email, password} → 200 {access_token}
GET /v1/users/{username} → 200 {profile object}
PATCH /v1/users/{id} {display_name?, bio?} → 200 {updated}
POST /v1/users/{id}/follow → 204
DELETE /v1/users/{id}/follow → 204
GET /v1/users/{id}/followers ?limit&cursor → 200 {paginated}
GET /v1/users/{id}/following ?limit&cursor → 200 {paginated}
# --- Tweet Service Endpoints ---
POST /v1/tweets {content, reply_to_id?} → 201 {tweet}
GET /v1/tweets/{id} → 200 {tweet}
DELETE /v1/tweets/{id} → 204
GET /v1/tweets/{id}/replies ?limit&cursor → 200 {paginated}
# --- Timeline Endpoint (reads from Redis cache) ---
GET /v1/timelines/home ?limit&cursor&user_id=me → 200 {paginated tweets}The Hidden Connections — Where Layers Constrain Each Other
This is the insight that separates strong candidates from average ones: the three layers are not independent. Each layer's decisions create constraints and requirements in the other two.
HLD decision forces LLD change
HLD: 'We will cache the home timeline in Redis to avoid expensive database joins for every page load.'
LLD implication: This means the tweet data stored in Redis must be self-contained (include author's username and avatar_url). If a user changes their display name, you now need a cache invalidation strategy. The LLD must add a 'display_name_updated_at' timestamp to track this.
LLD change forces API change
LLD: 'We deleted the bio field from the users table to simplify the schema.'
API implication: The GET /v1/users/{username} endpoint currently returns bio in its response. Every mobile client that was rendering that field now receives null. This is a breaking change. You must version the API (introduce /v2/) or communicate the deprecation with advance notice and a sunset date.
API change reveals HLD gap
API: 'We want to add a search endpoint: GET /v1/search?q=cats'
HLD implication: A full-text search endpoint cannot be served efficiently from the existing tweets PostgreSQL table unless we add full-text indexing (expensive and limited in PostgreSQL). This changes the HLD: we need to add Elasticsearch as a separate component, set up tweet indexing via the message queue, and design the security boundary around who can query the search index.
Integration Mistakes — Real Bugs That Come From Ignoring One Layer
You designed an API field called 'last_seen' (API layer) but never added a last_seen column to the users table (LLD layer). The API always returns null. Found in production three weeks after launch.
HLD says: cache user profiles in Redis for 1 hour. API does not have a cache invalidation mechanism (no endpoint to force-expire a profile). User changes display name, waits an hour for cache to expire. Users see stale name everywhere.
LLD uses an auto-incrementing integer for user IDs (1, 2, 3...). API exposes these in URLs as /v1/users/3. Security problem: crawlers can enumerate all users. HLD fix: use UUIDs or obfuscated IDs.
HLD decision: split into microservices. LLD of Tweet Service stores author_id but not author data (correct, separate service). But the API for timeline joins tweet data with author data and returns them together. Who does this join? The API Gateway? A new Timeline Service? The frontend with two separate requests? Nobody decided. Product breaks at launch.
The 45-Minute System Design Interview — Full Timeline
Most system design interviews give you 45-60 minutes. Here is how to allocate that time across all three layers while keeping the conversation structured:
Minutes 0-5: Requirements Clarification. Ask: How many users? Read-heavy or write-heavy? What features are in scope? What devices? Any specific latency requirements? What does success look like? Write the agreed requirements visibly.
Minutes 5-15: HLD Sketch. Draw the high-level architecture: client, CDN, API gateway, core services, databases, cache, queue. Name each box. Describe data flow for the 1-2 most critical user journeys (the happy path).
Minutes 15-25: Deep Dive 1 (LLD / Database Design). The interviewer will often say 'Let's go deeper on the database.' Design the core tables: columns, types, relationships, primary keys, important indexes. Explain your choices.
Minutes 25-35: Deep Dive 2 (Scaling / API). Address bottlenecks. How does the system handle peak load? Introduce sharding, read replicas, queue-based processing. Define 3-4 key API endpoints with request/response format.
Minutes 35-45: Discuss trade-offs and open questions. What would you do differently with more time? What are the weak points? What metrics would you monitor?
Cross-Lab Practice in ScaleDojo
ScaleDojo has three separate labs — Architecture Lab, LLD Lab, API Design Lab. The most effective practice technique is designing the same system in all three labs:
Start in the Architecture Lab. Design the HLD: services, data stores, traffic flow for a URL shortener.
Switch to the LLD Lab. Design the database schema for the URL shortener: the urls table, user table, click events table. Add the right indexes.
Switch to the API Design Lab. Design the API: POST /v1/urls, GET /{shortcode} (redirect), GET /v1/analytics/{shortcode}. Define status codes, pagination for analytics, authentication.
Connect them mentally: Notice how the schema columns become API response fields. Notice how the HLD's choice of read-replica affects which database the API reads from. Notice how the analytics use case requires a time-series append-only table (LLD) exposed through a paginated endpoint (API).
Practice Systems — Design These Across All Three Layers
URL Shortener (classic beginner system: bit.ly clone). ~4 hours to design fully.
Ride-sharing Service (complex: Uber/Lyft clone). Location updates, matching, surge pricing.
Social Media Feed (Instagram/Twitter timeline). Fan-out, caching, media upload.
E-Commerce Cart and Checkout (Amazon clone). Inventory, payment, order state machine.
File Storage Service (Google Drive / Dropbox). Chunked upload, deduplication, sharing.
System design mastery is not memorizing architectures. It is learning to see how HLD, LLD, and API Design constrain and inform each other — and making trade-offs explicitly.