System Design Fundamentals

Part of the System Design Interview Guide. This page covers the building blocks. Once you’re comfortable here, move to The 45-Min Interview Framework →

Everything you need to know before tackling specific HLD problems. These are the building blocks every design uses.


System Design Concepts — Learning Tracker

Track your progress through all 50 fundamentals. Sign in to save across devices.

Start Here

# Concept Deep Dive
1 Scalability (Vertical vs Horizontal) Read →

Core Infrastructure

# Concept Deep Dive
2 Load Balancing (Round Robin, L4 vs L7) Read →
3 CDN (Pull vs Push, Cache Invalidation) Read →
4 Proxy (Forward vs Reverse) Read →
5 API Gateway (Routing, Auth, Rate Limiting) Read →
6 Connection Pooling (HikariCP, PgBouncer) Read →
7 Networking Basics (TCP/UDP, HTTP/2 vs /3, DNS, TLS) Read →

Data & Storage

# Concept Deep Dive
9 Database Indexing (B-Tree, Composite) Read →
10 Database Sharding (Hash, Range, Geo) Read →
11 Database Replication (Primary-Replica, Quorum) Read →
12 DB Query Complexity (Postgres, DynamoDB, Redis) Read →
13 Object Storage (S3, Presigned URLs) Read →
14 Write-Ahead Log (Crash Recovery, Kafka) Read →
15 Transactions & Isolation Levels (ACID, MVCC, Write Skew) Read →

Caching & Performance

# Concept Deep Dive
16 Caching (Cache-Aside, Stampede, Eviction) Read →
17 Rate Limiting (Token Bucket, Sliding Window) Read →
18 Bloom Filters (Probabilistic, False Positives) Read →

Communication & Messaging

# Concept Deep Dive
19 Message Queues (Kafka vs SQS vs RabbitMQ) Read →
20 WebSocket vs SSE vs Polling Read →
21 Fan-Out Patterns (On Write vs On Read) Read →
22 Dead Letter Queue (Poison Messages, Retry) Read →

Distributed Systems

# Concept Deep Dive
23 CAP Theorem (CP vs AP, PACELC) Read →
24 Consistency Models (Strong, Eventual, Causal) Read →
25 Consistent Hashing (Hash Ring, Virtual Nodes) Read →
26 Leader Election (Raft, ZooKeeper, Split-Brain) Read →
27 Vector Clocks (Causality, Conflict Detection) Read →
28 Merkle Trees (Data Integrity, Anti-Entropy) Read →
29 Fencing Tokens (Stale Lock Prevention) Read →

Patterns & Architecture

# Concept Deep Dive
30 Event Sourcing & CQRS Read →
31 Saga Pattern (Choreography vs Orchestration) Read →
32 Outbox Pattern (Reliable Event Publishing) Read →
33 Circuit Breaker (Fail Fast, Fallback) Read →
34 Idempotency (Keys, Duplicate Prevention) Read →
35 Retry & Exponential Backoff (Jitter) Read →
36 Durable Execution (Temporal, Step Functions) Read →

Architecture Decisions

# Concept Deep Dive
37 Microservices vs Monolith Read →
38 Batch vs Stream Processing Read →
39 API Design (REST, GraphQL, gRPC) Read →
40 Service Discovery (DNS, Consul, K8s) Read →

Security & Auth

# Concept Deep Dive
41 Authentication (JWT, Sessions, OAuth 2.0) Read →

Performance & Operations

# Concept Deep Dive
42 Performance Metrics (p99, Little’s Law, SLO) Read →
43 Observability (Metrics vs Logs vs Traces, RED/USE) Read →
44 Deployment & Reliability (Canary, RPO/RTO, Migrations) Read →

Other Essentials

# Concept Deep Dive
45 Back-of-Envelope Estimation Read →
46 Heartbeat & Health Checks (Liveness, Readiness) Read →
47 Geospatial Indexing (Geohash, H3, Redis Geo) Read →
48 Distributed Locking (Redis, DynamoDB, ZK) Read →
49 Unique ID Generation (Snowflake, UUID v7, ULID) Read →

Reference

# Concept Deep Dive
50 Terminology & Fine Distinctions (180+ X-vs-Y answers) Read →

The summaries below serve as a quick reference. Click “Read →” on any topic above for the full deep dive with diagrams, code, and interview questions.


Fine Distinctions Interviewers Actually Ask

Interviewers rarely ask “what is sharding?” They ask “what’s the difference between partitioning and sharding?” — because the difference reveals whether you learned the concept or memorized the word. These are 15-second answers, and they’re heavily weighted.

The single most-asked one:

  Partitioning Sharding
Definition Splitting one dataset into smaller pieces Partitioning across separate machines
Scope Can be within one database/server Always spans multiple servers
Goal Manageability, query pruning, cheap archival Scale past one machine’s CPU/RAM/disk
Example Postgres PARTITION BY RANGE (created_at) — one server, 12 monthly child tables 4 Postgres instances, users 0-25M on box 1, 25-50M on box 2
Coordination cost None High (routing, cross-shard joins, distributed txns)

Say this: “Sharding is a type of partitioning — horizontal partitioning where the pieces live on different machines. All sharding is partitioning; not all partitioning is sharding. If I split a table by month but it’s still one Postgres instance, that’s partitioning: I get query pruning and cheap drops of old data, but zero extra write throughput.”

⚠️ “Partition” means three different things — a subset of table rows (databases), a network partition (CAP), or the unit of ordering and parallelism (Kafka). If it’s ambiguous, ask which one. That’s a point in your favor.

A dozen more, rapid-fire:

Question Your 15-second answer
Replication vs sharding? Replication copies the same data (read scale, availability); sharding splits different data (write scale, capacity). Production uses both.
Latency vs throughput? Time per op vs ops per second. Batching raises throughput and hurts latency. Linked by Little’s Law: concurrency = rate × latency.
ACID’s C vs CAP’s C? ACID-C = invariants hold after a transaction. CAP-C = replicas agree on the latest value. Same letter, unrelated properties.
Linearizability vs serializability? Recency on a single object vs a serial-equivalent order over transactions. Both together = strict serializability (Spanner).
At-least-once vs exactly-once? Exactly-once delivery is impossible; exactly-once processing = at-least-once + an idempotent consumer or dedupe key.
Queue vs topic? Queue = one consumer handles each message. Topic = every subscriber gets a copy.
Authentication vs authorization? Who you are (401) vs what you may do (403).
Forward vs reverse proxy? Forward sits in front of clients (they configure it). Reverse sits in front of servers (clients don’t know it exists).
Backpressure vs load shedding? Slow the producer down (keep the work) vs drop requests (protect the system).
Optimistic vs pessimistic locking? Version check on write and retry on conflict (low contention) vs lock up front (high contention).
SLO vs SLA? Internal target vs external contract with penalties. The SLA is always looser.
RPO vs RTO? How much data you can lose vs how long you can be down.
2PC vs Saga? 2PC = atomic, blocking, holds locks, coordinator is a SPOF. Saga = local transactions + compensations, no locks, intermediate states visible. Microservices use sagas.

All 180+ distinctions, grouped by area → — storage, consistency, performance, networking, messaging, auth, and architecture.


Scalability

Vertical scaling (scale up): Add more CPU/RAM to one machine. Simple but has a ceiling.

Horizontal scaling (scale out): Add more machines. Harder (state management, coordination) but practically unlimited.

  Vertical Horizontal
Cost Expensive hardware Cheap commodity servers
Limit Hardware ceiling Practically unlimited
Complexity Low High (distributed state)
Downtime Requires restart Zero-downtime rolling
When to use DB primary, cache Stateless services, web tier

Load Balancing

Distributes traffic across multiple servers so no single server gets overloaded.

Where it sits:

Client → Load Balancer → Server 1
                       → Server 2
                       → Server 3

Algorithms:

Algorithm How it works Best for
Round Robin Rotate through servers 1→2→3→1→… Equal-capacity servers
Weighted Round Robin More traffic to stronger servers Mixed hardware
Least Connections Send to server with fewest active requests Long-lived connections
IP Hash Same client always hits same server Session stickiness
Random Pick randomly Simple, surprisingly effective

L4 vs L7:

⚠️ Common mistake: Proposing a load balancer without explaining WHAT it’s balancing. Always specify: “L7 load balancer routing to 5 stateless API servers.” Also, don’t forget - if you’re using WebSockets, you need sticky sessions or L4 balancing because the connection is stateful.


Caching

Store frequently accessed data closer to the consumer. Trades freshness for speed.

Where to cache:

Client → CDN → API Gateway Cache → Application Cache → Database
         ↑         ↑                      ↑
     static     response-level       object-level
     assets     (full response)      (query results)

Cache strategies:

Strategy How Best for
Cache-Aside App checks cache, misses → read DB → write cache General purpose, most common
Write-Through Write to cache + DB together Strong consistency needs
Write-Behind Write to cache, async flush to DB later High write throughput
Read-Through Cache itself fetches from DB on miss Simpler app code

Cache eviction policies:

Cache invalidation (the hard problem):

Tools: Redis, Memcached, CDN (CloudFront, Cloudflare), local in-process (Caffeine, Guava)

⚠️ Common mistake: Caching everything. Only cache data that’s read frequently AND doesn’t change often. If you’re caching data that changes every request, you’re adding latency and complexity for no benefit. Also watch for cache stampede - when a popular key expires and 1000 requests simultaneously hit the DB to rebuild it. Solutions: locking (only one request rebuilds), early refresh (rebuild before TTL expires), or jittered TTLs.


Database Concepts

SQL vs NoSQL

  SQL (Postgres, MySQL) NoSQL (DynamoDB, Cassandra, MongoDB)
Schema Fixed, enforced Flexible, schema-on-read
Relationships Joins, foreign keys Denormalized, no joins
Scale Vertical (hard to shard) Horizontal (built for it)
Consistency Strong (ACID) Tunable (eventual to strong)
Best for Transactions, complex queries High throughput, simple access patterns

Database Replication

Primary-Replica: One primary handles writes. Replicas handle reads. Read-heavy workloads scale horizontally.

Writes → Primary DB ──replicates──→ Replica 1 (reads)
                                  → Replica 2 (reads)
                                  → Replica 3 (reads)

Replication lag: Replicas might be a few ms behind primary. If you write then immediately read from a replica, you might not see your write. Solutions: read-your-writes consistency, sticky sessions to primary after write.

Database Sharding (Partitioning)

Split data across multiple databases by a shard key.

User ID 1-1M    → Shard 1
User ID 1M-2M   → Shard 2
User ID 2M-3M   → Shard 3

Shard key choice is critical:

Problems with sharding:

⚠️ Common mistake: Proposing sharding too early. A single well-tuned Postgres with read replicas handles 50K+ TPS and multiple TB. Don’t shard until the math proves you need it. Also, always state your shard key and the tradeoff: “Shard by user_id - fast for user-scoped queries, expensive for global aggregations.”


CAP Theorem

In a distributed system, you can only guarantee 2 of 3:

In practice: Network partitions WILL happen. So you choose between:

Most systems are AP with tunable consistency - you choose per-operation whether you need strong or eventual consistency.

⚠️ Interview tip: Don’t pick CP or AP for your entire system. Different parts need different guarantees. “Product catalog is AP (eventual consistency fine), but inventory count is CP (can’t oversell).” That shows mature thinking.


Consistency Models

Model Guarantee Example
Strong Read always sees latest write Single-node DB, ZooKeeper
Eventual Read will eventually see latest write DynamoDB (default), Cassandra
Read-your-writes YOU see your own writes immediately; others might not Social media feeds
Causal If A caused B, everyone sees A before B Chat messages

Message Queues

Decouple producers from consumers. Enable async processing.

Producer → Queue → Consumer
           ↑
     (buffer, retry, ordering)

Why use queues:

Delivery guarantees:

Tools: Kafka (log-based, ordered, high throughput), SQS (simple queue, managed), RabbitMQ (routing, priority)

Kafka vs SQS:

  Kafka SQS
Ordering Per-partition guaranteed FIFO queue or best-effort
Retention Days/weeks (replay possible) 14 days max, once consumed gone
Throughput Millions/sec Thousands/sec
Consumer model Pull (consumer controls pace) Pull (long-polling)
Use case Event streaming, log aggregation Task queues, decoupling

⚠️ Common mistake: Using Kafka when SQS would suffice. If you just need “process this job later” with no ordering requirement and < 10K msgs/sec, SQS is simpler and cheaper. Kafka shines when you need ordering, replay, or millions of events/sec. Also - saying “put it in a queue” without specifying what the consumer does is hand-waving. Always say: “Consumer X reads from the queue and does Y.”


API Design

REST

GET    /users/123       → fetch user
POST   /users           → create user
PUT    /users/123       → replace user
PATCH  /users/123       → partial update
DELETE /users/123       → delete user

Key principles:

Rate Limiting

Protect services from abuse or thundering herds.

Algorithms:


Authentication - JWT and Sessions

Almost every system design includes “auth via JWT” in the API section. Here’s what that means and when to use which approach.

Session-based auth (traditional):

Client logs in → Server creates a session (stored in Redis/DB) → returns session ID as a cookie
Client sends cookie on every request → Server looks up session → "yes, this is user 42"

JWT (JSON Web Token) - stateless auth:

Client logs in → Server creates a signed token containing {userId: 42, exp: ...} → returns token
Client sends token in every request header → Server verifies the signature → "yes, this is user 42"

A JWT is a self-contained token with three parts:

header.payload.signature

Header:  { "alg": "HS256" }
Payload: { "userId": 42, "role": "admin", "exp": 1719500000 }
Signature: HMAC-SHA256(header + payload, SECRET_KEY)

When to use which:

  JWT Sessions
Scale Scales horizontally (no shared state) Needs centralized session store
Invalidation Hard (wait for expiry or maintain blocklist) Easy (delete from Redis)
Payload Can carry user data (role, permissions) Just an opaque ID
Best for Microservices, APIs, service-to-service Monoliths, web apps with logout needs

In system design interviews, default to JWT because it’s stateless and doesn’t require a session store. But mention the trade-off: “JWT can’t be revoked instantly - for sensitive operations like password change, we’d use a short-lived access token (15 min) + a long-lived refresh token stored server-side.”

Access + Refresh Token pattern (what most production systems use):

Login → get access token (15 min TTL) + refresh token (7 days, stored in DB)
Every API call → send access token (verified locally, no DB hit)
Access token expires → call /refresh with refresh token → get new access token
Logout → delete refresh token from DB (access token expires naturally in 15 min)

This gives you the scalability of JWT (no DB hit per request) with the revocability of sessions (delete refresh token = user is logged out within 15 min).

Interview tip: When asked “how do you handle auth?” say: “Short-lived JWT access token verified at the gateway, refresh token stored server-side for revocation. The gateway validates the signature without hitting a DB on every request.” That shows you understand both scalability and security.


CDN (Content Delivery Network)

Cache static content at edge locations close to users.

User in India → CDN edge in Mumbai (cache hit) → fast!
                  ↓ (cache miss)
              Origin server in US → slow, but CDN caches for next time

What to put on CDN: Images, CSS, JS, videos, static HTML, API responses (with TTL)

Tools: CloudFront, Cloudflare, Fastly, Akamai


Consistent Hashing

Problem: you have N cache servers. hash(key) % N works until you add/remove a server - then ALL keys remap.

Consistent hashing: Only K/N keys remap when a server is added/removed.

How: place servers on a ring (0 to 2^32). Hash the key → walk clockwise → first server you hit owns that key. Adding a server only steals keys from its clockwise neighbor.

Used in: DynamoDB, Cassandra, Redis Cluster, load balancers


Idempotency

An operation is idempotent if doing it 1 time or N times produces the same result.

Why it matters: In distributed systems, retries happen. If “charge $10” is retried, you don’t want to charge $20.

How to achieve:

Examples:


Heartbeat & Health Checks

How distributed systems detect dead nodes.

Failure detection trade-off:


Leader Election

When multiple nodes exist, sometimes one must be the “leader” (coordinates work, makes decisions).

Algorithms: ZooKeeper (ephemeral nodes), Raft (consensus), Bully algorithm

Why needed:


Back-of-Envelope Estimation

Quick math to validate design decisions.

Key numbers to memorize:

Operation Time
L1 cache read 1 ns
RAM read 100 ns
SSD read 100 μs
HDD seek 10 ms
Network round-trip (same DC) 0.5 ms
Network round-trip (cross-continent) 150 ms

Data size rules:

Traffic rules:


Database Indexing

Without an index, finding a user by email means scanning every row. With 10M users, that’s 10M rows checked. With an index, it’s milliseconds.

Types of indexes:

Index Type How Best For
B-tree (default) Sorted tree structure Exact lookups + range queries (dates, IDs)
Hash Direct hash → position Exact match only, faster than B-tree for equality
Full-text Inverted index of words Text search (“find all posts mentioning ‘Redis’”)
Geospatial (R-tree, GiST) Spatial partitioning “Restaurants within 5km”
Composite Multiple columns sorted together Queries filtering on city AND date

When to create an index:

When NOT to index:

External search indexes (Elasticsearch, Typesense): When your queries go beyond what your primary DB supports (full-text search, fuzzy matching, faceted filters), sync data via CDC (Change Data Capture) to a dedicated search engine. The search index lags slightly but enables queries your main DB can’t handle efficiently.

⚠️ Interview tip: If the problem has text search (“search for restaurants by name”), you NEED a search index. Don’t say “just use LIKE ‘%biryani%’ in SQL” - that’s a full table scan. Say “we’ll use Elasticsearch synced from the primary DB via CDC, accepting 1-2 second staleness on the search index.”


Database Query Complexity (What Interviewers Actually Ask)

In HLD interviews, you’ll be asked “how will you query this data?” Here’s what operations cost in the two most common databases.

Postgres (SQL) — Query Time Complexity

Operation Without Index With B-Tree Index Example
Find by primary key O(n) O(log n) WHERE id = 123
Find by any column O(n) full scan O(log n) if indexed WHERE email = 'x@y.com'
Range query O(n) O(log n + k) (k = results) WHERE age BETWEEN 20 AND 30
Sort (ORDER BY) O(n log n) O(n) if index matches sort ORDER BY created_at DESC
Filter + Sort O(n log n) O(log n + k) with composite index WHERE status='active' ORDER BY date
COUNT(*) O(n) O(n) even with index SELECT COUNT(*) FROM users
JOIN O(n x m) nested loop O(n + m) with indexes on join keys users JOIN orders ON user_id
LIKE ‘abc%’ O(n) O(log n) prefix uses index WHERE name LIKE 'Sha%'
LIKE ‘%abc%’ O(n) O(n) can’t use index WHERE name LIKE '%kumar%'

How B-Tree works (mental model):

Without index: flip through every page to find "Kumar" → O(n)
With B-Tree: sorted tree → jump to "K" section → O(log n)

         [M]
        /   \
     [D,H]   [R,W]
    / | \    / | \
  [A-C][E-G][I-L][N-Q][S-V][X-Z]

Lookup: O(log n) — traverse tree depth
Range: O(log n) to find start, then scan leaves sequentially

Composite Index Rule:

Index on (status, created_at) helps queries on:

Rule: composite index works left-to-right only.

DynamoDB (NoSQL) — Query Time Complexity

Operation Complexity Example
GetItem (PK + SK) O(1) PK=user_123, SK=order_456
Query (PK + sort key range) O(1) + O(k) PK=user_123, SK between '2025-01' and '2025-06'
Query with filter expression O(all items in partition) PK=user_123, filter: status='active' (reads all, filters after)
Scan (full table) O(N) entire table Avoid in production
Query on GSI O(1) + O(k) GSI PK=status, SK=created_at

Key concept: DynamoDB partition key = O(1) hash lookup to find the right partition. Sort key = sequential scan within that partition.

What DynamoDB can NOT do efficiently:

You want Limitation Solution
Filter by non-key attribute Scans entire partition Add a GSI
Sort by non-sort-key column Not possible GSI with desired sort key
Query across all users Full table scan O(N) GSI with that column as PK
JOINs Not supported Denormalize or app-side
Aggregation (SUM, COUNT) Not supported Streams → Lambda → aggregate
Full-text search Not supported Sync to Elasticsearch

GSI (Global Secondary Index):

Base table: PK = userId, SK = orderId
GSI:        PK = status, SK = createdAt

Query: "All PENDING orders sorted by date"
  Without GSI → Scan entire table O(N)
  With GSI → Query PK='PENDING' → O(1) + O(k)

Redis — Time Complexity

Operation Complexity Use Case
GET / SET O(1) Cache lookup
HGET / HSET O(1) Hash field access
ZADD / ZRANGEBYSCORE O(log n) Leaderboards, rate limiting
LPUSH / RPOP O(1) Queue
LINDEX (access by index) O(n) Avoid for large lists
SMEMBERS O(n) Get all set members
GEORADIUS O(n + log n) Nearby search

Quick Decision Framework (What to Say in Interview)

Access Pattern Best DB Why
“Get user by ID” Any (PK lookup) O(1) DynamoDB, O(log n) Postgres
“Get all orders for a user, sorted by date” DynamoDB (PK=userId, SK=date) or Postgres (indexed) Both efficient
“Find all active users” Postgres (indexed) or DynamoDB GSI Needs index either way
“Search by name substring” Elasticsearch Neither Postgres nor DDB handles this well
“Top 100 by score” Redis Sorted Set O(log n) insert, O(log n + k) range
“Count of orders per day” Postgres DDB can’t aggregate natively
“Real-time leaderboard” Redis ZSET O(log n) updates, O(log n + k) reads

⚠️ Interview tip: When interviewer asks “how will you query X?” — first identify if it’s a key-based lookup or a filter/sort. Key lookups are cheap everywhere. Filters on non-indexed columns are always O(n). The answer is almost always: “add an index” (Postgres) or “create a GSI” (DynamoDB).


Real-Time Communication (WebSocket vs SSE vs Polling)

When your system needs to push data to clients (chat messages, live tracking, notifications), you have three options:

Method Direction When to use Example
Polling Client → Server (repeated) Simple, low-frequency updates Email inbox check every 30s
Long Polling Client holds connection, server responds when ready Moderate real-time, simple infra Facebook’s original chat
SSE (Server-Sent Events) Server → Client (one-way) Server pushes, client only receives Live scores, stock tickers
WebSocket Bidirectional Both sides send freely Chat, collaborative editing, live games

Decision rule:

WebSocket challenges at scale:

⚠️ Common mistake: Proposing WebSockets when SSE or even long-polling would work. WebSockets add complexity (stateful connections, sticky routing, reconnect handling). Only use them when the CLIENT needs to push data to the server frequently (chat, collaborative editing). For server-only pushes (live scores, tracking), SSE is simpler and works with standard HTTP infrastructure.


Data Modeling: Normalization vs Denormalization

Normalization: Split data across tables to avoid duplication. One source of truth per entity.

users: { id, name, email }
orders: { id, user_id, product_id, amount }  ← references user by ID
products: { id, name, price }

Denormalization: Duplicate data to avoid joins. Optimize for read speed.

orders: { id, user_id, user_name, product_name, amount }  ← copies user_name into order

When to denormalize:

Safe default: Start normalized. Denormalize specific hot paths when you identify read bottlenecks.


Event Sourcing & CQRS

Used in: Stock Broker, Digital Wallet

Event Sourcing: Instead of storing current state, store every event that happened. Current state = replay all events.

Event 1: OrderPlaced { orderId: 123, amount: 500 }
Event 2: PaymentReceived { orderId: 123, amount: 500 }
Event 3: OrderShipped { orderId: 123, trackingId: "ABC" }

Current state of order 123 = SHIPPED (derived from replaying events)

Why: Complete audit trail, time-travel debugging, easy to add new read models retroactively.

CQRS (Command Query Responsibility Segregation): Separate the write path from the read path.

When to use:

When NOT to use: Simple CRUD apps. The complexity tax is high.

⚠️ Interview tip: Only propose event sourcing for financial or audit-heavy systems. For a URL shortener or chat app, it’s overkill. Say: “We use event sourcing here because every transaction needs a complete audit trail and we need multiple read models (portfolio view, tax report, P&L dashboard) from the same data.”


Saga Pattern

Used in: Digital Wallet, BookMyShow

A saga coordinates a multi-step distributed transaction where each step has a compensating action (undo).

Example - booking a trip:

Step 1: Reserve hotel → Compensate: Cancel hotel
Step 2: Book flight → Compensate: Cancel flight
Step 3: Charge card → Compensate: Refund card

If Step 3 fails, run compensations in reverse: cancel flight, cancel hotel.

Two approaches:

Orchestration Choreography
Central orchestrator coordinates all steps Each service listens to events and reacts
Easier to reason about, single point of control No single point of failure, but harder to trace
Better for complex flows (5+ steps) Better for simple flows (2-3 steps)

Key principle: Each step must be idempotent (safe to retry) and have a defined compensation.


Geospatial Indexing

Used in: Uber, Zomato

When you need “find things near this location” - restaurants within 3km, drivers within 5 minutes, friends nearby.

Approach How Used By
Geohash Encode lat/lng into a string. Nearby points share prefix. Elasticsearch, general
Redis Geo (GEOADD/GEORADIUS) In-memory sorted set with geohash encoding Uber, Grab, delivery apps
PostGIS Postgres extension with R-tree spatial index Low-write-volume geo queries
H3 (Hexagonal grid) Uniform-area hexagons, hierarchical resolution Uber surge pricing, analytics
S2 Geometry Hilbert curve cells, used by Google Google Maps, multi-level precision

Decision rule:


Bloom Filters

Used in: Key-Value Store

A space-efficient probabilistic structure that tells you: “definitely NOT here” or “MAYBE here.”

Why it matters: Before reading a 100MB file on disk to check if a key exists, ask the bloom filter first (1μs). If it says “not here,” skip the disk read entirely. Saves enormous I/O.

Properties:

Used in: LSM-tree databases (LevelDB, RocksDB, Cassandra), CDN cache lookups, spell checkers, duplicate detection.


Write-Ahead Log (WAL)

Used in: Key-Value Store, any database

Before applying a change to an in-memory data structure, first write it to a sequential log on disk.

Why: If the process crashes before the change is flushed to the main data file, the WAL can be replayed on restart to recover the lost data. Zero data loss.

The pattern:

  1. Write operation arrives
  2. Append to WAL (sequential disk write - fast)
  3. Apply to in-memory structure (memtable, buffer pool)
  4. Eventually flush in-memory changes to disk
  5. Truncate WAL up to the flushed point

Every database uses this: Postgres, MySQL, Redis (AOF), Kafka (the log IS the database).


Fan-Out Patterns

Used in: Twitter Feed, Instagram, Notification System

Fan-out = one event → many recipients.

Pattern When How
Fan-out on Write (push) Recipient count is small-medium (< 10K) On event, push to all recipients’ caches immediately
Fan-out on Read (pull) Recipient count is huge (celebrities, 50M followers) Do nothing on event; assemble at read time
Hybrid Mixed audience (most users small, some huge) Push for normal users, pull for celebrities

The decision threshold: If pushing takes > 5 seconds (too many recipients), switch to pull for that sender.


Merkle Trees

Used in: Key-Value Store

A Merkle tree is a hash tree used to efficiently detect differences between two copies of data. Instead of comparing every key one by one (O(N)), you compare hashes at each tree level (O(log N)).

How it works:

         Root Hash (abc...)
        /                  \
   Hash(left)          Hash(right)
   /       \           /        \
Hash(K1) Hash(K2)  Hash(K3)  Hash(K4)
  |         |        |          |
 K1        K2       K3         K4
  1. Each leaf is the hash of one key-value pair
  2. Each internal node is the hash of its children
  3. The root hash represents ALL data on the node

To find which keys diverged between two replicas:

  1. Compare root hashes. Different? → go deeper.
  2. Compare left and right children. Left matches, right doesn’t? → only check the right subtree.
  3. Recurse until you find the exact leaves that differ.

Why it matters: After a node failure and recovery, you need to sync it with a healthy replica. Without Merkle trees, you’d transfer ALL keys to check which are stale. With Merkle trees, you only transfer the specific keys that actually diverged - saving enormous bandwidth.

Used by: Cassandra (anti-entropy repair), DynamoDB, Git (internal object storage), IPFS, Ethereum.


Vector Clocks

Used in: Key-Value Store

A vector clock tracks causality between events in a distributed system. It tells you: “did event A happen before event B, or were they concurrent?”

The problem: In a distributed system with no global clock, two replicas can independently write to the same key. When they sync, which write is “newer”? Wall-clock timestamps are unreliable (clock skew), so we need a logical clock.

How it works:

Each node maintains a vector (array) of counters, one per node:

Node A writes: [A:1, B:0, C:0]
Node B writes: [A:0, B:1, C:0]

These are CONCURRENT - neither happened before the other.

Node A reads B's write and writes again: [A:2, B:1, C:0]
This HAPPENED AFTER B's write (A:2 > A:1 AND B:1 >= B:1).

Comparison rules:

What to do with conflicts:

⚠️ Interview tip: Most systems use LWW for simplicity. Mention vector clocks to show depth, but say “for our use case, LWW with a version counter is sufficient - vector clocks add complexity we don’t need unless we have multi-master writes.”


Fencing Tokens

Used in: Uber, BookMyShow, Job Scheduler

A fencing token prevents stale processes from corrupting data after their lock has expired.

The problem: Process A acquires a distributed lock (TTL = 30s). Process A pauses (GC, network delay) for 35 seconds. Lock expires. Process B acquires the same lock. Now Process A wakes up, thinks it still holds the lock, and writes - corrupting Process B’s work.

The solution: Every lock acquisition returns a monotonically increasing fencing token (a number). Any downstream write must include the token. The resource rejects writes with a token older than the latest one it’s seen.

Process A gets lock → token = 33
Process A pauses...
Lock expires. Process B gets lock → token = 34
Process A wakes up, tries to write with token 33
Resource sees: 33 < 34 (latest seen) → REJECTED

Where to apply: Any system where distributed locks protect a shared resource: seat booking, order assignment, job execution.


Outbox Pattern

Used in: Digital Wallet, Stock Broker

The outbox pattern solves: “how do I update my database AND publish an event atomically?”

The problem: You want to save an order to the DB and send an event to Kafka. If the DB write succeeds but Kafka publish fails (or vice versa), your systems are inconsistent.

The solution:

  1. Write both the business data AND the event to the SAME database transaction (the event goes into an “outbox” table)
  2. A separate process (CDC or poller) reads the outbox table and publishes events to Kafka
  3. Once published, mark the outbox row as processed
BEGIN TRANSACTION
  INSERT INTO orders (...) 
  INSERT INTO outbox (event_type, payload) VALUES ('OrderCreated', '{...}')
COMMIT

-- Separate process (CDC/poller):
-- Reads outbox → publishes to Kafka → marks as sent

Why not just publish to Kafka directly? Because you can’t atomically commit to Postgres AND Kafka in one transaction. The outbox makes it a local DB transaction (atomic), then handles the Kafka publish separately with retries.

CDC (Change Data Capture) is the production approach: tools like Debezium watch the database transaction log and stream changes to Kafka automatically. No polling needed.


Durable Execution (Temporal / Cadence)

Used in: Uber, Zomato, Job Scheduler

Temporal (formerly Cadence, open-sourced by Uber) is a framework for running long-lived, multi-step workflows that survive crashes.

The problem: A food delivery dispatch involves: offer to rider → wait for response (15s) → if rejected, try next rider → if accepted, notify customer → track delivery. Any step can fail. If a server crashes mid-workflow, the whole dispatch is lost.

How Temporal solves it:

You write workflow code as a normal function. Temporal records every step. If the process crashes, it replays from the last checkpoint - your workflow continues exactly where it left off.

// Pseudocode - this survives crashes
function dispatchOrder(orderId) {
  riders = findNearbyRiders(orderId)
  for rider in riders:
    offer = sendOffer(rider, timeout=15s)  // persisted step
    if offer.accepted:
      notifyCustomer(orderId, rider)       // persisted step
      return
  escalateToOps(orderId)
}

When to use:

When NOT to use:

⚠️ Interview tip: Mention Temporal when the interviewer asks “what if the server crashes mid-workflow?” It shows you know production tools. But don’t over-engineer - for simple retry logic, a dead-letter queue is enough.


Rate Limiting

Controls how many requests a client can make in a given time window. Used to protect APIs from abuse, prevent DDoS, and enforce fair usage.

Algorithms

Algorithm How it works Pros Cons
Token Bucket Bucket fills at fixed rate. Each request removes a token. If empty, reject. Allows bursts, smooth rate Slight complexity
Leaky Bucket Requests enter a queue that processes at fixed rate. Perfectly smooth output No burst tolerance
Fixed Window Count requests in fixed time windows (e.g., per minute). Simple Burst at window edges (2x in 1 sec)
Sliding Window Log Store timestamp of each request. Count within last N seconds. Exact Memory-heavy (stores every timestamp)
Sliding Window Counter Combine current + previous window with time-weighted average. Low memory, ~99% accurate Approximate

Token Bucket (most common in interviews):

Bucket capacity: 10 tokens
Refill rate: 1 token/second

Request arrives:
  - tokens > 0? → allow, tokens -= 1
  - tokens == 0? → reject (429 Too Many Requests)

After 10 seconds of no requests → bucket is full again (10 tokens)

Where to place rate limiter:

Distributed rate limiting: Use Redis with Lua scripts for atomic check-and-decrement across multiple server instances.

⚠️ Interview tip: Always mention WHERE the rate limiter sits (edge/gateway, not inside each microservice) and that it’s stateful (needs shared store like Redis for distributed deployments).


Distributed Locking

When multiple services/instances need to coordinate access to a shared resource (e.g., only one worker should process a job, only one transfer should debit a wallet).

Approaches

Method How Pros Cons
Redis SETNX SET lock_key "owner" NX EX 30 Simple, fast Single point of failure
Redlock Acquire lock on majority of Redis nodes (3/5) Tolerates node failures Complex, debated
DB row lock SELECT FOR UPDATE or conditional write ACID, no extra infra Slow, DB becomes bottleneck
DynamoDB conditional write PutItem with condition attribute_not_exists(lockKey) Serverless-friendly Eventually consistent reads
ZooKeeper / etcd Ephemeral nodes with watches Battle-tested, auto-cleanup Heavy infra, complex

Redis distributed lock pattern:

# Acquire
SET "lock:order_123" "worker_A" NX EX 30
# NX = only if not exists
# EX 30 = auto-expire in 30 seconds (prevents dead locks if holder crashes)

# Release (only if you're the owner)
if GET "lock:order_123" == "worker_A":
    DEL "lock:order_123"

Key problems solved:

⚠️ Interview tip: When you say “distributed lock”, always mention the TTL/expiry. Without it, a crashed holder keeps the lock forever. Also mention fencing tokens if discussing writes to storage — a lock alone doesn’t prevent stale operations.


Circuit Breaker Pattern

Prevents cascading failures when a downstream service is down. Instead of endlessly retrying and timing out, “break the circuit” and fail fast.

States:

CLOSED (normal) → requests flow through
    ↓ (failures exceed threshold)
OPEN (broken) → all requests fail immediately, no calls to downstream
    ↓ (after timeout period)
HALF-OPEN (testing) → allow one request through
    ↓ success → back to CLOSED
    ↓ failure → back to OPEN

When to use: Any call to an external service (payment provider, third-party API, downstream microservice) that might be slow or down.

Configuration:

Real-world: Netflix Hystrix (deprecated), Resilience4j (Java), Polly (.NET)

⚠️ Interview tip: Mention circuit breaker when designing payment gateways or any multi-service architecture. Shows you think about failure modes, not just the happy path.


Retry and Exponential Backoff

When a request fails due to a transient error (network blip, 503, timeout), don’t immediately retry at full speed — you’ll overwhelm the recovering service.

Exponential backoff:

Attempt 1: wait 1 second
Attempt 2: wait 2 seconds
Attempt 3: wait 4 seconds
Attempt 4: wait 8 seconds
(give up after max retries)

With jitter (add randomness):

wait = base_delay * 2^attempt + random(0, 1000ms)

Jitter prevents thundering herd — 1000 clients all retrying at exactly the same time.

What to retry vs what NOT to retry:

Retriable NOT retriable
500 (server error) 400 (bad request — your fault)
503 (service unavailable) 401 (unauthorized)
Timeout 404 (not found)
Connection refused 409 (conflict — duplicate)

⚠️ Interview tip: When you mention retries in HLD, always pair it with idempotency. Retrying a non-idempotent operation (like payment charge) without idempotency keys = double-charging the user.


Dead Letter Queue (DLQ)

When a message in a queue fails processing after multiple retries, instead of losing it or blocking the queue, move it to a separate “dead letter queue” for investigation.

Main Queue → Consumer tries to process
    ↓ (fails 3 times)
Dead Letter Queue → message sits here for manual review or auto-retry later
    ↓
Alert fires → engineer investigates

Why you need it:

Real-world: SQS DLQ, Kafka .DLT topics, RabbitMQ dead-letter exchange

⚠️ Interview tip: Always mention DLQ when designing async pipelines (notification system, payment processing, ETL). Shows you handle failure gracefully instead of silently dropping messages.


Service Discovery

In a microservices architecture with many services scaling up/down, how does Service A know the IP/port of Service B?

Approaches:

Method How Example
DNS-based Services register with DNS. Clients resolve hostname. AWS Route 53, Cloud Map
Registry-based Central registry (services register on start, deregister on stop). Clients query registry. Consul, Eureka, ZooKeeper
Sidecar/Mesh Each service has a proxy that handles routing. Istio, Envoy, Linkerd
Load Balancer All services sit behind LB. Clients talk to LB. AWS ALB, Nginx

In practice (most common in interviews): “Service A calls Service B through an internal load balancer or API gateway. Service discovery is handled by the platform (Kubernetes DNS, AWS Cloud Map).” — Don’t over-explain unless asked.


API Gateway

A single entry point for all client requests. Routes to the right microservice, handles cross-cutting concerns.

What it does:

Client → API Gateway → User Service
                     → Order Service
                     → Payment Service

Why not let clients call services directly?

Real-world: Kong, AWS API Gateway, Nginx, Envoy


Proxy: Forward vs Reverse

  Forward Proxy Reverse Proxy
Sits between Client and internet Internet and server
Client knows? Yes (configured) No (transparent)
Purpose Privacy, filtering, caching for clients Load balancing, SSL, caching for servers
Example VPN, corporate proxy Nginx, CDN, API Gateway

Reverse proxy in HLD (what you’ll use):

Internet → Reverse Proxy (Nginx) → Server 1
                                  → Server 2
                                  → Server 3

It’s basically what a load balancer does + SSL termination + static file caching.


Microservices vs Monolith

  Monolith Microservices
Deployment One big deployment Each service deploys independently
Scaling Scale everything together Scale only what’s needed
Complexity Simple to start Complex (networking, debugging, consistency)
Data Shared database Each service owns its data
Failure One bug takes down everything Isolated failures (with circuit breakers)
Team Works for small teams (< 10) Enables large teams to work independently
When to use MVP, early stage, small team Large teams, independent scaling needs

What to say in interview: “I’d start with a modular monolith and extract services only when a specific module needs independent scaling or a different team owns it.” Shows maturity.


Batch vs Stream Processing

  Batch Stream
When Process accumulated data at intervals Process each event as it arrives
Latency Minutes to hours Milliseconds to seconds
Example Nightly analytics, monthly reports Real-time fraud detection, live dashboards
Tools Spark, Hadoop, AWS Glue Kafka Streams, Flink, Spark Streaming
Complexity Lower Higher (ordering, exactly-once)

When to use which:

Common interview pattern:

Events → Kafka (stream) → Real-time consumer (live alerts)
                        → Batch consumer (nightly aggregation to data warehouse)

Connection Pooling

Creating a new database connection is expensive (~50-100ms for TCP + TLS + auth). Instead, maintain a pool of pre-created connections and reuse them.

Without pooling:
  Request 1: create connection → query → close
  Request 2: create connection → query → close (50ms wasted each time)

With pooling:
  Startup: create 20 connections, keep them alive
  Request 1: borrow connection → query → return to pool
  Request 2: borrow connection → query → return to pool (0ms overhead)

Configuration:

Why it matters in HLD: If your service has 100 instances each with max 50 connections, that’s 5000 connections to the DB. Most databases cap at ~5000-10000. This is why you need connection pooling + potentially a connection proxy (PgBouncer for Postgres).


Object Storage (S3 Design Principles)

For storing large files (images, videos, backups, logs) — not rows of data.

Key properties:

Access patterns: | Operation | Latency | Cost | |—|—|—| | PUT (upload) | ~100ms | Low | | GET (download) | ~50-100ms | Low | | LIST (list all objects) | Slow for large buckets | Expensive | | DELETE | Fast | Free |

When to use in HLD:

Presigned URLs: Generate a time-limited URL that lets a client upload/download directly to S3 without going through your server. Reduces server load for large files.

⚠️ Interview tip: When your design involves file uploads, say “Client uploads directly to S3 via a presigned URL to avoid bottlenecking our API servers.” Then S3 triggers an event (via SNS/SQS) for post-processing (thumbnail generation, virus scan, etc).


Numbers Every Engineer Should Know

Quick reference for capacity planning:

System Throughput Notes
Single Postgres 10-50K TPS Depends on query complexity
Single Redis 100-200K ops/sec In-memory, single-threaded
Single Kafka broker 200K-1M msgs/sec Depends on message size
Single Elasticsearch node 10-30K writes/sec Depends on mapping complexity
HTTP request (same DC) 1-5ms roundtrip Network + processing
Cross-continent roundtrip 100-200ms Speed of light limit
SSD random read 100μs 10K IOPS typical
RAM access 100ns 1000x faster than SSD

Storage rules:


How to Approach an HLD Interview

  1. Clarify requirements (2-3 min): functional + non-functional. Ask what’s in scope.
  2. Back-of-envelope (2 min): traffic, storage, bandwidth estimates.
  3. High-level design (10-15 min): boxes and arrows. Client → LB → Service → DB.
  4. Deep dive (15-20 min): interviewer picks 2-3 areas. Show depth.
  5. Wrap up (2-3 min): trade-offs, what you’d change at 10× scale.

Don’t:


Next: Read the 45-min interview framework → to learn how to deliver these concepts in an actual interview. Or pick a specific design problem and see them applied.


Part of the System Design Interview Guide

Step Page
1. Prep Roadmap Interview Guide
2. Fundamentals You’re here
3. Interview Framework The 45-Min Approach →
4. Practice HLD Problems →

Free system design + DSA prep. If it helped you crack an interview, consider supporting.