System Design Fundamentals

65System Design Fundamentals

What you will master here

  • How to approach any system design interview
  • Scaling: vertical, horizontal, partitioning, replication
  • Caching, CDNs, load balancers
  • Databases: SQL vs NoSQL; sharding; replication
  • Async patterns: queues, pub-sub, event sourcing
  • CAP theorem and consistency models
  • Failures: timeouts, retries, circuit breakers, idempotency
  • Designing a URL shortener, chat app, news feed — quick walkthroughs

65.1 The interview structure

  1. Clarify — users, scale, features. "Are we designing read-heavy or write-heavy? Global or single-region?"
  2. Estimate — back-of-envelope. "1M DAU, 10 reads each = 10M reads/day = ~120 read QPS average, 1200 peak."
  3. Data model — what entities, what relationships, primary keys.
  4. API — endpoints with example request/response.
  5. High-level diagram — client, LB, service, DB, cache, queue.
  6. Deep dive — pick the hardest part, work it out.
  7. Trade-offs — SQL vs NoSQL? Sync vs async? Strong vs eventual consistency?
  8. Failures & scale — how does it handle 10× growth? A region outage?

65.2 Scaling fundamentals

Vertical vs horizontal

Stateless services + load balancer

Make app servers stateless (no session in process memory). Store state in DB or Redis. Now you can run N replicas behind a load balancer — round-robin or least-connections. Adding capacity = launching more instances.

Sharding (partitioning)

Split data across multiple DB instances by a key (user_id mod N, geographic region, hash range). Each shard handles 1/N of the load.

Replication

65.3 Caching

Cache layerWhat it cachesTool
BrowserStatic assets, imagesCache-Control headers
CDN (CloudFront, Cloudflare)Static assets globallyEdge servers
App / Service cache (in-memory)Frequently read DB rowsLocal hashmap
Distributed cacheShared across instancesRedis, Memcached
DB query cacheRecent query resultsBuilt-in to some DBs

Cache strategies

Cache invalidation — one of the two hard problems"There are only two hard things in CS: cache invalidation and naming things." Stale cache returns wrong data. Strategies: short TTL, event-driven invalidation on writes, cache versioning (key includes a version).

65.4 SQL vs NoSQL

PropertySQL (relational)NoSQL
SchemaFixedFlexible / schemaless
JoinsNative, powerfulLimited / none
Transactions (ACID)StrongOften eventual / per-document
ScalingVertical → harder horizontalHorizontal by design
ExamplesPostgres, MySQLMongoDB (document), Redis (KV), Cassandra (wide-column), Neo4j (graph)
Best forStructured data, complex queries, transactionsHigh write throughput, flexible schema, simple access patterns

65.5 Async patterns

Message queue (SQS, RabbitMQ)

Producer publishes message to a queue. Consumer pulls and processes. Decouples producer from consumer; smooths bursts; survives consumer crashes.

Pub-sub (Kafka, Redis Streams, Google Pub/Sub)

Producer publishes to a topic; many subscribers each get a copy. One event triggers many side effects (email + analytics + warehouse).

Event sourcing

Store every state change as an immutable event; current state is derived by replaying events. Auditable; complex; great for financial systems.

CQRS

Command Query Responsibility Segregation — different models for writes and reads. Writes go to a normalized DB; reads from a denormalized projection optimised for queries.

65.6 CAP theorem

In a distributed system, you can have at most two of:

Since network partitions happen in real systems, you really choose CP or AP. SQL DBs default CP (refuse writes during partition); Dynamo / Cassandra default AP (accept writes, reconcile later).

65.7 Consistency models

65.8 Failure handling

65.9 Quick walkthroughs

Design a URL shortener

Design a chat app (1-1)

Design a news feed

Module 38 — System Design Q&A

Vertical vs horizontal scaling?
Vertical: bigger machine — simple, hits a ceiling. Horizontal: more machines — harder (state, load balancing), scales arbitrarily. Real systems combine both: vertical for the DB, horizontal for stateless app servers.
What is sharding?
Splitting one dataset across multiple DB instances by key. Each shard owns 1/N of the data. Hash sharding for uniform load; range sharding for sorted access; directory-based for flexibility. Sharding is hard to undo — pick the key carefully.
What is CAP theorem?
A distributed system can have at most two of Consistency, Availability, Partition tolerance. Real systems must tolerate partitions, so you choose CP (refuse writes during partition) or AP (accept writes, reconcile later).
SQL vs NoSQL — when each?
SQL when you have structured data, complex queries, transactional integrity. NoSQL when you need horizontal scalability, flexible schema, very high write throughput, or simple access patterns. Many systems use both.
What is eventual consistency?
All replicas converge to the same state given enough time without writes. Common in NoSQL (Dynamo, Cassandra). Cheap, high availability, but reads may return stale data briefly.
What's a circuit breaker?
A pattern that monitors failures to a downstream service. After N failures, "opens" — fails fast for a cooldown period, then "half-opens" to test. Prevents cascading failure and gives the downstream room to recover.
Write-through vs cache-aside?
Write-through: every write updates both cache and DB. Cache always fresh; slower writes. Cache-aside: app checks cache, on miss reads DB and populates cache. Faster writes; stale risk until TTL or invalidation.
Why do we use a CDN?
To serve static assets from edge servers close to users — drastically reduces latency, offloads origin servers, absorbs traffic spikes. Cache-Control headers govern what's cached at the edge.
What's the difference between a queue and pub-sub?
Queue: one producer → one consumer per message (delivered to exactly one consumer). Pub-sub: one producer → topic → many subscribers each get a copy. Queues distribute work; pub-sub broadcasts events.
What's a load balancer doing?
Receives traffic, routes to one of N backend instances by round-robin, least-connections, or hash. Performs health checks; removes unhealthy instances. Layer 4 (TCP) or Layer 7 (HTTP — content-aware).
What is idempotency in API design?
Calling the same operation multiple times has the same effect as once. GET/PUT/DELETE are idempotent. POST isn't — but you can make it idempotent with an Idempotency-Key header. Crucial for safe retries on network errors.
What's a bottleneck and how do you find it?
The component that limits overall throughput. Find via metrics: CPU, memory, network, DB query time, lock contention. Profile under load; address the slowest layer first, then the next.
How do you handle a 10× traffic spike?
(1) Autoscale stateless services horizontally. (2) Cache aggressively — Redis, CDN. (3) Queue writes so DB isn't overwhelmed. (4) Rate-limit / shed load gracefully. (5) Prepared via load tests; not invented in the moment.
What's read-your-writes consistency?
After a user writes, they see their own write on subsequent reads (even from replicas). Implemented by routing the user's reads to the leader for a while, or by sticky sessions. Prevents the confusing "I just saved that, where did it go?" UX.
One-sentence approach to system design interviews?
"Clarify requirements + scale → estimate numbers → data model → API → high-level diagram → deep-dive one piece → discuss trade-offs and failure modes."

65.10 Capacity estimation cheat sheet (memorise)

ItemOrder of magnitude
L1 cache1 ns
L2 cache10 ns
Main memory100 ns
SSD random read100 µs
HDD seek10 ms
Round-trip same DC0.5 ms
Round-trip cross-region30-100 ms
Round-trip continent-to-continent150 ms
Single Postgres instance5-20K simple reads/sec
Redis single instance100K+ ops/sec
Single Node.js process10-50K simple-JSON req/sec
1 KB packet across continent~150 ms
Daily users → peak QPSpeak ≈ avg × 5-10; avg ≈ DAU × calls/user/day / 86400

65.11 Sharding strategies (deep)

StrategyProsCons
Hash sharding (hash(key) mod N)Even distributionRebalancing on N change moves nearly all keys
Range sharding (key ranges)Range queries cheapHot spots if data skewed; manual rebalancing
Consistent hashingRebalancing on N change moves ~1/N of keysSlight imbalance; need virtual nodes
Directory-based (lookup service)Most flexible, dynamic rebalancingExtra hop + central lookup is SPoF
Geographic / tenant-basedLocality, regulatory complianceCross-shard queries expensive

65.12 Replication patterns (deep)

65.13 Consistency models (deep)

ModelGuaranteeCost
LinearizableAs if single copy; reads return latest committed writeSlowest; requires coordination
SequentialAll clients see same order, not necessarily real-timeCheaper than linearizable
CausalCausally related ops ordered consistentlyVector clocks; medium cost
Read-your-writesUser sees own writes immediatelyRoute reads to leader for a window
Monotonic readsOnce you see a value, never see olderSticky session
EventualReplicas converge given no writesCheapest; always available

65.14 CAP + PACELC

65.15 Distributed system pitfalls

(These are the "Fallacies of Distributed Computing".)

65.16 Common system-design walkthroughs (interview)

Rate limiter

Unique id generator (Twitter Snowflake)

URL shortener

Chat (1-1 + group)

Distributed cache

Search autocomplete

News feed

Distributed lock

Notification system

65.17 Architectural styles

StyleWhen fits
MonolithSmall team, fast iteration, simple ops
Modular monolithSame codebase but enforced module boundaries; transition path
MicroservicesMultiple teams need independent deploy; bounded contexts
SOACoarser-grained services (older terminology)
ServerlessEvent-driven, sporadic load, pay-per-use
Event-drivenDecoupled producers/consumers; async workflows
CQRS + Event SourcingAudit trail required, complex projections, replay needed

65.18 Failure handling toolkit

Module 38 — System Design Q&A (extended)

Consistent hashing — explain in 1 minute
Map keys + nodes onto a ring (hash space 0..2^32). Key goes to the next node clockwise. Adding/removing a node only moves keys in that arc — ~1/N of keys, not all. Virtual nodes (each node placed at multiple ring positions) smooth distribution.
PACELC vs CAP?
PACELC adds: when there's NO partition, you still choose between Latency and Consistency. Real systems make trade-offs constantly, not just during partitions. Better captures reality.
How to design a rate limiter for a global API?
Per-edge enforcement (local in-memory counter) for low latency. Central Redis as source of truth for global limits. Use sliding window counter (approximation) for cost. Return 429 + Retry-After. Document limits.
Strong vs eventual consistency — when each?
Strong: financial transactions, inventory deduction, critical state where stale = wrong. Eventual: feeds, view counts, recommendations, analytics where stale is fine.
What's a saga pattern?
Distributed transaction split into local steps with compensating actions. If step 4 fails, run inverse of steps 1-3. Orchestrated (central state machine, Temporal/Camunda) or choreographed (each service reacts to events).
Outbox pattern, why?
Atomic write of DB row + event. Without it: "write DB then publish" can fail mid-step → divergence. Outbox: write event row in same DB TX as business row; poller reads outbox → publishes to broker → marks sent. Guarantees at-least-once.
How would you scale a write-heavy table?
(1) Vertical scale + tune autovacuum, indexes. (2) Read replicas (only helps reads). (3) Partition by time/tenant. (4) Shard horizontally (Citus or app-level sharding). (5) Move to NoSQL designed for writes (Cassandra). Each step is expensive — make sure earlier ones won't suffice.
How does Kafka guarantee ordering?
Within a partition, strict order. Across partitions, no global order. Key-based partitioning (hash(key) % N) sends same key to same partition → ordered per key. For total order, use 1 partition (no parallelism).
How would you design a global rate limiter with 99.99% availability?
Per-edge token bucket (eventual sync). Central Redis cluster (sharded by user) as global source. Edges allow approximate over-limit on partition. Aggregator reconciles every minute. Acceptable trade-off: slightly over the documented limit during partitions, never blocks legitimate traffic.
What's idempotency-key pattern?
Client generates a UUID per logical request, sends as header. Server stores key → response. Retry with same key returns cached response, doesn't re-execute. Used by Stripe, AWS APIs. Critical for at-least-once delivery contexts.
How to handle thundering herd?
N clients hit the same expired cache simultaneously, all miss, all hit DB. Solutions: single-flight (one fetches, others wait), probabilistic early expiration, jittered TTLs, lock-on-recompute pattern.
What's the difference between L4 and L7 load balancer?
L4 (TCP): routes by IP/port, doesn't inspect content. Fast, simple, protocol-agnostic. L7 (HTTP): routes by path/header/cookie; supports SSL termination, A/B routing, rewriting. Use L7 for HTTP services; L4 for DBs / TCP services.
Designing a notification service — biggest trade-offs?
Latency vs cost (push provider $$). At-least-once vs at-most-once (dedup vs missed). Per-user rate limiting vs spike absorption. Hot user (millions of notifications) vs avg user. Reliability of cross-provider failover.
What is read-after-write consistency?
User sees their own writes on subsequent reads, even from replicas. Implementations: route the user's reads to leader for N seconds; sticky session; client cache the write. Without it, "I just clicked Save and it's gone" UX bug.
How does a leader election work?
Consensus protocol (Paxos, Raft). Nodes vote on a candidate; majority wins. Heartbeat from leader; if absent, follower becomes candidate. ZooKeeper, etcd, Consul provide it as a service. Don't roll your own.
How would you design YouTube's video upload + view pipeline?
Upload → S3 → transcode pipeline (multiple bitrates) → CDN. Metadata in Postgres; views/likes in Cassandra (append-heavy). HLS/DASH adaptive streaming. Recommendations: offline ML + real-time signals. Search: Elasticsearch.
What's the typical bottleneck in a web service?
Usually database. Then network (egress bandwidth, latency). Then GC pauses or thread starvation. Profile with metrics; intuition lies. CPU on app servers rarely the limit before DB.
How to choose between SQL and NoSQL?
SQL if: complex queries, joins, transactions, schema enforced. NoSQL if: massive horizontal scale, flexible schema, simple access patterns, high write throughput. Most apps start with Postgres and never need NoSQL.
What's the biggest mistake in system design interviews?
Diving into details without clarifying requirements. Always start with scale + use cases + SLA. The same problem at 10K users vs 1B users has totally different solutions.
How to design a global presence service (who's online)?
Redis SET per region with TTL (30s). Client heartbeats every 15s. To check presence: query all regions in parallel OR replicate to a global SET. Trade-off: cost vs freshness. For exact "online count", use HyperLogLog for approximate cardinality.

65.X Code Patterns

Cache-Aside Pattern — Python + Redis

import redis, json, hashlib
from functools import wraps

cache = redis.Redis(host="localhost", port=6379, decode_responses=True)

def cached(ttl: int = 300):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            key = f"{fn.__name__}:{hashlib.md5(str(args).encode()).hexdigest()}"
            hit = cache.get(key)
            if hit:
                return json.loads(hit)
            result = fn(*args, **kwargs)
            cache.setex(key, ttl, json.dumps(result))
            return result
        return wrapper
    return decorator

@cached(ttl=60)
def get_user_profile(user_id: int) -> dict:
    # DB query here
    return {"id": user_id, "name": "Alice", "plan": "pro"}

Token Bucket Rate Limiter — Redis Lua

import redis, time

r = redis.Redis(host="localhost", port=6379)

RATE_LIMIT_SCRIPT = """
local key      = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate     = tonumber(ARGV[2])
local now      = tonumber(ARGV[3])
local tokens   = tonumber(redis.call("GET", key) or capacity)
local last_ts  = tonumber(redis.call("GET", key..":ts") or now)
local elapsed  = now - last_ts
local refill   = math.floor(elapsed * rate)
tokens = math.min(capacity, tokens + refill)
if tokens >= 1 then
    redis.call("SET", key, tokens - 1)
    redis.call("SET", key..":ts", now)
    redis.call("EXPIRE", key, 3600)
    return 1
else
    return 0
end
"""

rate_limit = r.register_script(RATE_LIMIT_SCRIPT)

def allow_request(user_id: str, capacity: int = 10, rate: float = 1.0) -> bool:
    allowed = rate_limit(
        keys=[f"rl:{user_id}"],
        args=[capacity, rate, int(time.time())]
    )
    return bool(allowed)

# Usage
if not allow_request("user_42", capacity=100, rate=10):
    raise Exception("429 Too Many Requests")

Circuit Breaker — Python

import time
from enum import Enum
from functools import wraps

class State(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=30):
        self.state = State.CLOSED
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.opened_at = None

    def __call__(self, fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            if self.state == State.OPEN:
                elapsed = time.time() - self.opened_at
                if elapsed > self.recovery_timeout:
                    self.state = State.HALF_OPEN
                else:
                    raise Exception("Circuit open — call blocked")
            try:
                result = fn(*args, **kwargs)
                if self.state == State.HALF_OPEN:
                    self.state = State.CLOSED
                    self.failure_count = 0
                return result
            except Exception as e:
                self.failure_count += 1
                if self.failure_count >= self.failure_threshold:
                    self.state = State.OPEN
                    self.opened_at = time.time()
                raise
        return wrapper

cb = CircuitBreaker(failure_threshold=3, recovery_timeout=60)

@cb
def call_payment_service(order_id: str):
    import requests
    return requests.post("https://payments.internal/charge", json={"order": order_id}, timeout=2)

Consistent Hashing — Python

import hashlib, bisect

class ConsistentHashRing:
    def __init__(self, nodes: list, replicas: int = 150):
        self.replicas = replicas
        self.ring: dict[int, str] = {}
        self.sorted_keys: list[int] = []
        for node in nodes:
            self.add_node(node)

    def _hash(self, key: str) -> int:
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def add_node(self, node: str):
        for i in range(self.replicas):
            h = self._hash(f"{node}:{i}")
            self.ring[h] = node
            bisect.insort(self.sorted_keys, h)

    def remove_node(self, node: str):
        for i in range(self.replicas):
            h = self._hash(f"{node}:{i}")
            del self.ring[h]
            self.sorted_keys.remove(h)

    def get_node(self, key: str) -> str:
        h = self._hash(key)
        idx = bisect.bisect(self.sorted_keys, h) % len(self.sorted_keys)
        return self.ring[self.sorted_keys[idx]]

ring = ConsistentHashRing(["db1", "db2", "db3", "db4"])
print(ring.get_node("user:42"))    # db2
print(ring.get_node("user:9999"))  # db1