System Design
Scalability, CAP, sharding, microservices.
Part 32: System Design
Learning Objectives
You will learn how to take a vague requirement like "design Twitter" and convert it into a concrete, defensible architecture. You will understand scalability constraints, the CAP theorem, caching strategies, database choices, and the engineering trade-offs between monoliths and microservices.
Why Does This Exist?
Junior engineers write code. Senior engineers design systems. The difference is that senior engineers think about: What happens when this gets 100x more users? What fails first? How do we recover? System design is the discipline of making these decisions deliberately before they are forced on you by production incidents.
The System Design Framework
Every system design starts with the same process: 1. REQUIREMENTS ├── Functional: What does the system do? └── Non-functional: How fast? How available? How consistent? 2. SCALE ESTIMATION ├── Daily Active Users (DAU) ├── Requests per second (RPS) ├── Data volume (GB/day) └── Read:Write ratio 3. HIGH-LEVEL DESIGN ├── Identify core components ├── Data flow between components └── Initial API design 4. DEEP DIVE ├── Database schema ├── Critical algorithms └── Bottleneck identification 5. TRADE-OFFS └── Every decision has a cost. State it explicitly.
The CAP Theorem
In a distributed system that experiences a network partition, you must choose between Consistency and Availability. You cannot have both.
CAP Theorem: ┌─────────────────────────────────────────────────────┐ │ Consistency │ │ Every read sees the latest write │ │ ↑ │ │ Must choose one side │ │ when partition occurs │ │ │ │ │ Availability ─────────┼──────────── Partition │ │ Every request │ Tolerance │ │ gets a response │ (always needed) │ └─────────────────────────────────────────────────────┘ CP Systems (PostgreSQL, Zookeeper): → Stay consistent but may reject requests during partition AP Systems (Cassandra, DynamoDB): → Stay available but may return stale data during partition
Scalability Patterns
VERTICAL SCALING
└── Bigger machine (more CPU, RAM)
✓ Simple, no code changes
✗ Has a ceiling, single point of failure, expensive
HORIZONTAL SCALING
└── More machines behind a load balancer
✓ Linear capacity growth
✗ State management complexity
The hardest part of horizontal scaling: STATE
┌─────────────────────────────────────────────────────┐
│ Stateless services scale horizontally easily │
│ Stateful services (sessions, cache) need extra work │
└─────────────────────────────────────────────────────┘
Solution: Move state outside the application
Sessions → Redis
Files → Object storage (S3)
Database → Dedicated cluster with replication
Caching Architecture
Cache levels from fastest to slowest: ┌─────────────────────────────────────────────┐ │ L1: In-process memory (Map/object) │ │ L2: Redis / Memcached (shared, networked) │ │ L3: CDN edge cache (geographic proximity) │ │ L4: Database query cache │ └─────────────────────────────────────────────┘ Cache strategies: READ-THROUGH: App reads cache → miss → load from DB → cache WRITE-THROUGH: Write to cache AND DB simultaneously WRITE-BEHIND: Write to cache only → async flush to DB CACHE-ASIDE: App manages cache explicitly (most common) Cache invalidation strategies: TTL: Cache expires after N seconds Event-driven: Invalidate cache on write events Versioning: Cache key includes version (cache-v2:user:123)
Load Balancing
Load balancing algorithms: ┌──────────────────┬────────────────────────────────────┐ │ Round Robin │ Rotate requests equally │ │ Least Connections│ Route to server with fewest active │ │ IP Hash │ Same client → same server (sticky) │ │ Weighted │ Route more to powerful servers │ └──────────────────┴────────────────────────────────────┘ Health checks: Load balancer probes each server every N seconds. Removes unhealthy servers from rotation automatically.
Database Scaling
PRIMARY → REPLICA REPLICATION Primary: Handles all writes Replicas: Handle reads (eventual consistency) Use case: Read-heavy workloads (most web apps) SHARDING (Horizontal Partitioning) User 1-1M → Shard A User 1M-2M → Shard B User 2M-3M → Shard C Trade-offs: Cross-shard queries become complex, resharding is painful WHEN TO SHARD: - Single server cannot hold all data - Single server cannot handle write throughput - Data has natural partition key (userId, regionId)
Monolith vs Microservices
MONOLITH
All features in one deployable unit.
✓ Simple deployment, easy debugging, low latency (in-process calls)
✓ Easy refactoring (single codebase)
✗ Scales as one unit (can't scale auth independently of payments)
✗ Large teams collide on same codebase
MODULAR MONOLITH (The underrated middle ground)
Single deployable unit but with strict internal module boundaries.
└── /src/modules/auth/ (owns its own data layer)
└── /src/modules/payment/ (never calls auth directly)
└── /src/modules/users/ (communicates through interfaces)
✓ Refactoring is possible, deployment is simple
✓ Can split into microservices later when you actually need to
MICROSERVICES
Each bounded domain is a separate deployable service.
✓ Independent deployment and scaling
✓ Technology choice per service
✗ Network latency between every call
✗ Distributed transactions are genuinely hard
✗ Massive operational overhead (N services × complexity)
RULE: Start with a monolith. Extract services when you have a
proven reason. Conway's Law: Your architecture mirrors your org.
Real Engineering Challenge: Design a URL Shortener
You need to design bit.ly. 100M URLs created per day. 10B redirects per day. URLs must redirect in under 10ms.
View Architecture
Scale estimates:
- 100M writes/day = ~1,200 writes/sec
- 10B reads/day = ~115,000 reads/sec (read-heavy!)
- Read:Write = ~95:1
Short code generation:
- Base62 (a-z, A-Z, 0-9) = 62 chars
- 7 chars = 62^7 = 3.5 trillion unique codes
Architecture:
Browser → CDN Edge Cache → Load Balancer → Read Service
↓
Redis Cache (hot URLs)
↓ cache miss
PostgreSQL (all URLs)
Write flow:
Browser → API → Generate code → Write to DB → Return short URL
Read flow (the critical path):
Browser → CDN checks cache (hit? → 301 redirect)
→ Cache miss → Redis (hit? → redirect + refresh cache)
→ DB miss → PostgreSQL → return 301 → cache in Redis
Database schema:
urls (
id BIGSERIAL PRIMARY KEY,
short_code VARCHAR(10) UNIQUE NOT NULL,
long_url TEXT NOT NULL,
user_id BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
click_count BIGINT DEFAULT 0
)
CREATE INDEX idx_short_code ON urls(short_code); ← critical
Mini Project (20-30 min)
Design a basic Rate Limiter using a token bucket algorithm.
▶ View Solution
class TokenBucket {
tokens: number;
lastRefill: number;
constructor(public capacity: number, public refillRate: number) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
allowRequest(): boolean {
const now = Date.now();
const refill = Math.floor((now - this.lastRefill) / 1000) * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + refill);
this.lastRefill = now;
if (this.tokens > 0) {
this.tokens--;
return true;
}
return false;
}
}
Bigger Project (1-2 hours)
Design and implement a scalable URL Shortener service. Include capacity planning (storage, bandwidth), a distributed ID generator (like Snowflake), and a caching layer (Redis) to handle heavy read loads while maintaining high availability.
\n▶ View Solution
Client -> Load Balancer -> Web Tier -> App Tier -> Primary DB
|-> Cache (Redis)
|-> Object Storage (S3) -> CDN
Interview Questions
\nCore: How do you choose between a relational and non-relational database?
Relational databases (SQL) are best for structured data, complex joins, and strong ACID guarantees (e.g., financial transactions). Non-relational databases (NoSQL) are better for flexible schemas, rapid iteration, and horizontal scaling for high write loads or massive datasets.
Hard: How do you handle database replication lag in a read-heavy system where read-after-write consistency is required?
You can route reads to the primary database immediately after a write for a short window (e.g., 500ms) or for that specific user's session. Alternatively, version the data and if a replica returns a stale version, fallback to reading from the primary. Another approach is synchronous replication, though it trades off write availability and latency.
Senior: Design a system to prevent cache stampede (thundering herd) when a highly requested item expires.
I would use a combination of techniques: 1) Probabilistic Early Expiration (PER) where a background process updates the cache just before it expires. 2) Mutex Locks: When the cache misses, the first thread acquires a lock to query the DB and update the cache, while other threads wait or return stale data. 3) Stale-While-Revalidate: Serve stale data immediately while asynchronously refreshing it in the background.
Revision Sheet
- Requirements first: Functional + Non-functional (scale, latency, availability)
- CAP: CP or AP during partition — choose based on your domain
- Scale state out: Sessions → Redis, Files → S3, DB → replicas
- Monolith first: Extract microservices only when you have proven need
- Cache aggressively: Most web reads can be cached at some layer