InterviewHack.ai
Start free
Blog/System Design Interview Questions: How to Answer Them (40 Questions)

System Design Interview Questions: How to Answer Them (40 Questions)

September 16, 2026

system-designbackend

Complete 8,000+ word article on System Design Interview Questions with 40 detailed questions, real code, tradeoff analysis, and preparation frameworks. Covers foundational concepts through senior-level topics.

System Design Interview Questions: How to Answer Them (40 Questions)

System design interviews are the gate that separates mid-level engineers from senior ones. Unlike coding rounds, there is no single correct answer — the interviewer wants to see how you think, how you handle ambiguity, and whether you can navigate the classic tradeoffs that every production system forces you to make.

This guide covers 40 real system design interview questions with detailed answers, real code where it matters, and the frameworks you need to reason through them. Whether you are interviewing at FAANG, a Series B startup, or anywhere in between, these are the problems that show up repeatedly.


How to Structure Every System Design Answer

Before the questions, internalize this framework. Every answer should hit these beats in roughly this order:

  1. 1Clarify requirements — Ask about scale, consistency guarantees, read vs. write ratio, latency SLAs, and geographic distribution. Never start designing before you know what you are designing *for*.
  2. 2Estimate scale — Back-of-the-envelope math: daily active users, requests per second, storage needed, bandwidth.
  3. 3Define the API — What endpoints or interfaces does the system expose? This forces you to define inputs and outputs before you design internals.
  4. 4High-level design — Draw the happy path first: client → load balancer → app servers → database.
  5. 5Deep dive on components — Pick the hardest part (usually data model, consistency, or the hot path) and go deep.
  6. 6Identify bottlenecks and mitigations — Where does the system break at 10x scale? What do you do about it?
  7. 7Discuss tradeoffs — Every design decision has a cost. Name the cost explicitly.

Do not memorize answers. Internalize the tradeoffs and use this framework to reason out loud.


The Core Tradeoffs You Must Know Cold

These concepts appear in almost every system design question. Knowing them deeply is the prerequisite.

| Concept | When it comes up |

|---|---|

| CAP theorem | Any distributed storage or coordination question |

| SQL vs. NoSQL | Data model and scale questions |

| Eventual vs. strong consistency | Any write-heavy or multi-region system |

| Synchronous vs. asynchronous processing | Any high-throughput ingestion pipeline |

| Horizontal vs. vertical scaling | Bottleneck analysis |

| Read replicas | Any read-heavy system |

| Sharding | Any write-heavy or very large dataset |

| Caching (CDN, edge, application, DB) | Any latency or hot-data problem |

| Message queues | Decoupling producers from consumers |

| Rate limiting | Any public API or abuse prevention question |


Part 1 — Foundational System Design Questions

1. Design a URL shortener (like bit.ly)

Clarifying questions to ask first: How many URLs are shortened per day? How long should short codes be? Do we need analytics? Custom slugs? Expiration?

Scale estimation:

  • 100M URLs shortened per day = ~1,160 writes/second
  • 10B redirects per day = ~116,000 reads/second
  • Read:write ratio ~100:1 — this is very read-heavy
  • Each URL record: ~500 bytes → 100M/day × 365 = ~18TB/year

The core design problem: generate a short, unique, collision-resistant code for each URL.

Two approaches for code generation:

*Approach A — Counter + Base62 encoding*

A global counter (stored in Redis or a dedicated counter service) increments for each new URL. Encode the counter in base62.

python
import string

BASE62 = string.ascii_letters + string.digits  # 62 chars

def encode(n: int) -> str:
    if n == 0:
        return BASE62[0]
    result = []
    while n:
        result.append(BASE62[n % 62])
        n //= 62
    return ''.join(reversed(result))

def decode(s: str) -> int:
    result = 0
    for char in s:
        result = result * 62 + BASE62.index(char)
    return result

# encode(12345678) → "5lurZ" (6 chars covers 56B URLs)

Problem: the counter is a single point of failure and a write bottleneck. Mitigate with range-based counter allocation (each app server claims a block of 1M IDs from Redis or Zookeeper, works locally until exhausted).

*Approach B — Random code with collision check*

python
import secrets
import string

def generate_code(length=7):
    alphabet = string.ascii_letters + string.digits
    return ''.join(secrets.choice(alphabet) for _ in range(length))

# On write: generate code, check DB, retry if collision
# Collision probability at 7 chars: negligible until ~3B URLs

Data model:

sql
CREATE TABLE urls (
    short_code  VARCHAR(10) PRIMARY KEY,
    long_url    TEXT NOT NULL,
    user_id     BIGINT,
    created_at  TIMESTAMP DEFAULT NOW(),
    expires_at  TIMESTAMP,
    click_count BIGINT DEFAULT 0
);

CREATE INDEX idx_long_url ON urls(long_url);  -- for dedup check

High-level architecture:

Client
  ↓
CDN (cache redirect responses with Cache-Control: 301, max-age=3600)
  ↓
Load Balancer
  ↓
App Servers (stateless, horizontal scale)
  ↓
Redis Cache (short_code → long_url, TTL = 24h)
  ↓ (cache miss)
PostgreSQL Primary (writes) + Read Replicas (reads)

Key tradeoffs:

  • Use 301 (permanent redirect) to offload traffic to the client, but you lose click analytics. Use 302 (temporary redirect) if you need to count every click — every request hits your servers.
  • Cache the mapping aggressively (80% of traffic goes to 20% of URLs). A Zipf distribution means caching the top 20% of URLs handles ~80% of redirects.
  • For analytics: write click events to Kafka asynchronously, batch-process into ClickHouse or BigQuery. Never block the redirect path for analytics writes.

2. Design a rate limiter

Clarifying questions: Per user? Per IP? Per API key? What are the limits (requests per second, minute, hour)? Distributed (multiple servers)? Hard vs. soft limits?

Five algorithms to know:

| Algorithm | Pros | Cons |

|---|---|---|

| Token Bucket | Handles bursts, smooth | Complex state |

| Leaky Bucket | Smooth output rate | Bursty input wasted |

| Fixed Window Counter | Simple | Boundary burst problem |

| Sliding Window Log | Accurate | Memory intensive |

| Sliding Window Counter | Accurate + memory efficient | Slightly approximate |

Token Bucket implementation in Redis (distributed):

lua
-- Lua script runs atomically on Redis
local key = KEYS[1]
local rate = tonumber(ARGV[1])      -- tokens per second
local capacity = tonumber(ARGV[2])  -- max tokens in bucket
local now = tonumber(ARGV[3])       -- current timestamp (ms)
local requested = tonumber(ARGV[4]) -- tokens needed for this request

local bucket = redis.call("HMGET", key, "tokens", "last_refill")
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now

-- Refill tokens based on elapsed time
local elapsed = (now - last_refill) / 1000.0
tokens = math.min(capacity, tokens + elapsed * rate)

if tokens >= requested then
    tokens = tokens - requested
    redis.call("HMSET", key, "tokens", tokens, "last_refill", now)
    redis.call("EXPIRE", key, 3600)
    return 1  -- allowed
else
    redis.call("HMSET", key, "tokens", tokens, "last_refill", now)
    redis.call("EXPIRE", key, 3600)
    return 0  -- denied
end

Sliding window counter (recommended for most cases):

python
import redis
import time

r = redis.Redis()

def is_allowed(user_id: str, limit: int, window_seconds: int) -> bool:
    now = time.time()
    window_start = now - window_seconds
    key = f"ratelimit:{user_id}"
    
    pipe = r.pipeline()
    # Remove events outside the window
    pipe.zremrangebyscore(key, 0, window_start)
    # Count events in current window
    pipe.zcard(key)
    # Add current event
    pipe.zadd(key, {str(now): now})
    # Set TTL to clean up old keys
    pipe.expire(key, window_seconds * 2)
    results = pipe.execute()
    
    current_count = results[1]
    return current_count < limit

Architecture for a distributed rate limiter:

Request → API Gateway
  → Extract identifier (user_id, API key, IP)
  → Rate Limit Service
      → Redis Cluster (consistent hashing, same user always hits same shard)
      → Return Allow/Deny + Retry-After header
  → Forward to backend (if allowed) or return 429 (if denied)

Response headers to always include:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 743
X-RateLimit-Reset: 1735689600
Retry-After: 3600  (only on 429)

Key tradeoff: Redis gives you atomic operations via Lua scripts, but you are adding a network hop to every request. Mitigate with local caching (allow locally, sync to Redis every N requests) at the cost of slight over-counting. For most APIs, slight over-counting is acceptable.


3. Design a key-value store (like Redis)

Clarifying questions: What operations are needed (GET, SET, DELETE, TTL)? What consistency guarantees? Single node or distributed? Persistence?

Core data structures for a simple in-memory key-value store:

python
import time
import threading
from collections import defaultdict
from typing import Optional

class KeyValueStore:
    def __init__(self):
        self._store: dict = {}
        self._expiry: dict = {}
        self._lock = threading.RLock()
    
    def set(self, key: str, value, ttl_seconds: Optional[int] = None):
        with self._lock:
            self._store[key] = value
            if ttl_seconds is not None:
                self._expiry[key] = time.time() + ttl_seconds
            elif key in self._expiry:
                del self._expiry[key]
    
    def get(self, key: str):
        with self._lock:
            if key in self._expiry and time.time() > self._expiry[key]:
                del self._store[key]
                del self._expiry[key]
                return None
            return self._store.get(key)
    
    def delete(self, key: str) -> bool:
        with self._lock:
            if key in self._store:
                del self._store[key]
                self._expiry.pop(key, None)
                return True
            return False

For a distributed key-value store, the three hardest problems are:

  1. 1Data partitioning — Use consistent hashing so adding/removing nodes rebalances a minimal fraction of keys
  2. 2Replication — How many copies? Synchronous (strong consistency, higher latency) or asynchronous (eventual consistency, lower latency)?
  3. 3Conflict resolution — When two nodes have conflicting values for the same key, who wins? Last-write-wins (LWW) is simple but lossy. Vector clocks track causality but are complex.

Consistent hashing:

python
import hashlib
from bisect import bisect, insort

class ConsistentHash:
    def __init__(self, replicas=150):
        self.replicas = replicas
        self.ring = []
        self.nodes = {}
    
    def add_node(self, node: str):
        for i in range(self.replicas):
            virtual_node = f"{node}:{i}"
            h = int(hashlib.md5(virtual_node.encode()).hexdigest(), 16)
            insort(self.ring, h)
            self.nodes[h] = node
    
    def remove_node(self, node: str):
        for i in range(self.replicas):
            virtual_node = f"{node}:{i}"
            h = int(hashlib.md5(virtual_node.encode()).hexdigest(), 16)
            self.ring.remove(h)
            del self.nodes[h]
    
    def get_node(self, key: str) -> str:
        h = int(hashlib.md5(key.encode()).hexdigest(), 16)
        idx = bisect(self.ring, h) % len(self.ring)
        return self.nodes[self.ring[idx]]

Quorum reads and writes (Dynamo-style):

  • N = total replicas, W = write quorum, R = read quorum
  • Strong consistency: W + R > N (e.g., N=3, W=2, R=2)
  • High availability: W=1, R=1 (eventual consistency)
  • Typical choice: N=3, W=2, R=2

4. Design a message queue (like Kafka)

Clarifying questions: What throughput? At-least-once or exactly-once delivery? Ordering guarantees? How long to retain messages? Consumer groups?

Core concepts:

  • Topics — logical channels
  • Partitions — the unit of parallelism; each partition is an ordered, immutable log
  • Offset — position of a message in a partition (consumers track their own offset)
  • Consumer groups — multiple consumers can read the same topic independently; within a group, each partition is consumed by exactly one consumer

Why partitions matter for scale:

A single partition is a sequential log — you get ordering but no parallelism. By partitioning, you can have P consumers processing in parallel. The tradeoff: ordering is only guaranteed *within* a partition, not across partitions.

Topic: "user-events"
  Partition 0: [e1, e4, e7, e10, ...]
  Partition 1: [e2, e5, e8, e11, ...]
  Partition 2: [e3, e6, e9, e12, ...]

Consumer Group A:
  Consumer A1 → reads Partition 0
  Consumer A2 → reads Partition 1
  Consumer A3 → reads Partition 2

Consumer Group B (independent):
  Consumer B1 → reads all 3 partitions (if only 1 consumer in group)

Producer with partition key (ensures ordering per user):

python
from kafka import KafkaProducer
import json

producer = KafkaProducer(
    bootstrap_servers=['kafka:9092'],
    value_serializer=lambda v: json.dumps(v).encode('utf-8'),
    acks='all',              # wait for all replicas (durability)
    retries=3,
    compression_type='gzip'
)

def publish_user_event(user_id: str, event: dict):
    # partition_key ensures all events for same user go to same partition
    # → ordering guaranteed per user
    producer.send(
        topic='user-events',
        key=user_id.encode('utf-8'),  # partition key
        value=event
    )

publish_user_event("user-123", {"type": "page_view", "url": "/dashboard"})

Consumer with manual offset commit (at-least-once delivery):

python
from kafka import KafkaConsumer
import json

consumer = KafkaConsumer(
    'user-events',
    bootstrap_servers=['kafka:9092'],
    group_id='analytics-service',
    value_deserializer=lambda m: json.loads(m.decode('utf-8')),
    enable_auto_commit=False,  # manual commit for control
    auto_offset_reset='earliest'
)

for message in consumer:
    try:
        process_event(message.value)
        consumer.commit()  # only commit after successful processing
    except Exception as e:
        # don't commit — message will be redelivered
        log_error(e)

Key tradeoffs:

  • Sync vs. async writes: acks='all' is durable but adds ~5-10ms latency. acks=1 is faster but you can lose messages on leader failure.
  • Partition count: more partitions = more parallelism, but more overhead. A good heuristic: partitions = 2× expected consumers.
  • Replication factor = 3 for production. Never 1.

5. Design a consistent hashing ring

Already covered the implementation above. Key interview points:

Without consistent hashing: adding a node to N nodes means remapping ~K/N keys (where K = total keys). With 1M keys and 10 nodes, that's 100K remapped keys per node addition.

With consistent hashing: adding a node remaps ~K/N keys on average — same fraction, but crucially, only the keys that "fall between" the new node and its predecessor on the ring are remapped.

Virtual nodes solve the uneven distribution problem. Without them, if your hash function clusters nodes unevenly, some nodes get much more traffic. With 150 virtual nodes per physical node, the distribution approaches uniform.


Part 2 — Product-Scale System Design

6. Design Twitter/X (social feed)

This is the fan-out problem. When a user with 10M followers tweets, do you write to 10M timelines immediately (fan-out on write), or compute each user's timeline on read (fan-out on read)?

Fan-out on write (push model):

User tweets
  → Write to tweet table (async)
  → Enqueue fan-out job
  → Fan-out worker reads follower list (in batches of 1000)
  → For each follower: append tweet_id to their timeline cache in Redis
  
Read timeline:
  → Fetch tweet_ids from Redis (O(1) per user)
  → Fetch tweet details by IDs (cache-friendly)
  → Return merged timeline

Pros: reads are fast (pre-computed). Cons: a celebrity with 10M followers makes every tweet a massive write amplification event.

Fan-out on read (pull model):

User tweets
  → Write to tweet table
  
Read timeline:
  → Fetch list of accounts user follows
  → Query each account's recent tweets (since last read)
  → Merge and sort by time

Pros: writes are cheap. Cons: reads are expensive — O(following_count) queries per timeline load.

Twitter's actual approach: hybrid

  • Regular users (
  • Celebrities (>X followers, called "heavy hitters"): fan-out on read, injected at read time
  • Timeline assembly: take cached timeline (from write fan-out) + fetch celebrity tweets (from read fan-out) + merge
Timeline Request:
  1. Fetch pre-computed timeline from Redis
  2. Identify celebrities user follows
  3. For each celebrity: fetch their recent tweets
  4. Merge + deduplicate + sort
  5. Return top N

Data model:

sql
CREATE TABLE tweets (
    tweet_id    BIGINT PRIMARY KEY,  -- snowflake ID (time-sortable)
    user_id     BIGINT NOT NULL,
    content     TEXT NOT NULL,
    created_at  TIMESTAMP NOT NULL,
    reply_to_id BIGINT,
    retweet_id  BIGINT
);

CREATE TABLE follows (
    follower_id  BIGINT NOT NULL,
    followee_id  BIGINT NOT NULL,
    created_at   TIMESTAMP NOT NULL,
    PRIMARY KEY (follower_id, followee_id)
);

CREATE INDEX idx_tweets_user ON tweets(user_id, created_at DESC);
CREATE INDEX idx_follows_followee ON follows(followee_id);  -- for fan-out

Snowflake ID generation (time-sortable, no coordination needed):

64-bit ID structure:
[41 bits: timestamp ms] [10 bits: machine ID] [12 bits: sequence]

Max throughput: 4096 IDs/ms per machine
IDs are roughly time-sorted — good for feed ordering

7. Design YouTube

The dominant design challenge is video storage and streaming, not social features.

Upload pipeline:

1. User uploads raw video (direct to object storage via presigned URL)
2. Upload service validates and enqueues transcoding job
3. Transcoding workers (GPU instances) convert to multiple resolutions:
   - 1080p, 720p, 480p, 360p, 240p
   - Multiple codecs: H.264 (wide compatibility), VP9/AV1 (better compression)
4. Output segments stored as HLS (.m3u8 + .ts chunks) or DASH
5. CDN origins pull from object storage; CDN edges cache popular segments
6. Video metadata written to DB; thumbnail generated
7. Notification sent to subscribers (async, via Kafka)

Why HLS/DASH? Adaptive bitrate streaming (ABR):

Player downloads manifest:
  720p: /video/abc/720p/index.m3u8
  480p: /video/abc/480p/index.m3u8
  360p: /video/abc/360p/index.m3u8

Player monitors bandwidth in real time.
If bandwidth drops → switches to lower quality playlist.
If bandwidth improves → switches to higher quality.
User gets smooth playback without buffering.

Storage math:

  • 500 hours of video uploaded per minute
  • 1 hour of 1080p ≈ 4GB raw; after transcoding to 5 resolutions ≈ 8GB total
  • 500 × 60 × 8GB = 240TB/hour of new content
  • This is why YouTube runs on Google Cloud with exabyte-scale object storage

CDN strategy:

  • Edge nodes cache the most popular video segments
  • Cache-hit rate for top 10% of videos: ~99%
  • Long-tail videos: served from origin (cost trade-off: don't pay to cache content nobody watches)

8. Design Uber (ride sharing)

The core problem: real-time matching of supply (drivers) and demand (riders).

Location tracking:

python
# Driver app sends location every 4 seconds
# Not every update needs to go to the DB

# Architecture:
# Driver → WebSocket → Location Service → Redis (current location)
#                                       → Kafka (location stream for analytics)
# Writes: ~1M drivers × 15 updates/min = 250K writes/sec to Redis

# Redis Geo commands (built-in geospatial index)
import redis
r = redis.Redis()

# Driver sends location update
r.geoadd("active_drivers", (longitude, latitude, driver_id))

# Rider requests a ride at (lat, lng)
# Find all drivers within 5km
nearby_drivers = r.georadius(
    "active_drivers",
    longitude=rider_lng,
    latitude=rider_lat,
    radius=5,
    unit="km",
    withdist=True,
    sort="ASC",
    count=20
)

Matching algorithm:

1. Find N nearest available drivers (Redis GEO query)
2. Rank by: estimated arrival time + driver rating + acceptance rate
3. Offer to top-ranked driver (timeout: 10 seconds)
4. If declined/timeout → offer to next driver
5. On acceptance: create trip, notify both parties

The dispatch problem at scale:

Divide the map into hexagonal cells (H3 from Uber is the industry standard). Each cell is managed by a stateless "supply manager" that tracks drivers in that cell.

python
import h3

def get_cell(lat: float, lng: float, resolution: int = 9) -> str:
    # Resolution 9 ≈ hexagons of ~0.1 km² area
    return h3.geo_to_h3(lat, lng, resolution)

def find_nearby_cells(lat: float, lng: float, rings: int = 2) -> list:
    center = get_cell(lat, lng)
    # k-ring returns all hexagons within k rings of center
    return list(h3.k_ring(center, rings))

Surge pricing: demand/supply ratio per H3 cell, updated every minute. If demand > supply threshold → multiply base price. Store current surge multipliers in Redis with TTL.


9. Design WhatsApp / a messaging app

Core challenges: message delivery guarantees, online/offline status, end-to-end encryption, message ordering.

Delivery states:

Sent (server received) → Delivered (recipient device received) → Read (recipient opened)

Architecture:

Client A                 Server                    Client B
  |                         |                         |
  |--- send(msg, to=B) --→  |                         |
  |                         |-- B online? ----------→ |
  |                         |  ← yes ---------------  |
  |← ack(SENT) ------------ |-- push(msg) ----------→ |
  |                         |← ack(DELIVERED) -------- |
  |← notification(DELIVERED)|                         |
  |                         |← ack(READ) ------------ |  (B opens chat)
  |← notification(READ) ----|                         |

Message storage:

sql
CREATE TABLE messages (
    message_id   BIGINT PRIMARY KEY,  -- snowflake
    conversation_id BIGINT NOT NULL,
    sender_id    BIGINT NOT NULL,
    content      BYTEA NOT NULL,      -- encrypted payload
    sent_at      TIMESTAMP NOT NULL,
    delivered_at TIMESTAMP,
    read_at      TIMESTAMP
);

-- Offline message queue (temporary)
CREATE TABLE pending_deliveries (
    message_id   BIGINT NOT NULL,
    recipient_id BIGINT NOT NULL,
    retry_count  INT DEFAULT 0,
    PRIMARY KEY (message_id, recipient_id)
);

Why not store messages in DB long-term:

WhatsApp famously stores very little on servers. Messages are delivered and deleted. This reduces storage costs and is a privacy feature. Clients are responsible for their own message history.

Group messaging fan-out:

  • Group of 256 people sends a message
  • Server stores message once (in object storage for media)
  • Server pushes to each online member's WebSocket connection
  • For offline members: store in pending_deliveries queue, deliver when they reconnect
  • Receipt tracking: server waits for delivery acks from each member before notifying sender

WebSocket connection management:

Each server holds N WebSocket connections. A connection map (in Redis or a dedicated service) maps user_id → server_id. When server A needs to push a message to a user connected to server B, it uses inter-server messaging (Kafka or direct gRPC).


10. Design Google Search (simplified)

This question tests whether you understand web crawling, indexing, and ranking at scale.

Three systems to design:

1. Crawler:

python
from collections import deque
import threading

class WebCrawler:
    def __init__(self, seed_urls: list, max_depth: int = 3):
        self.queue = deque([(url, 0) for url in seed_urls])
        self.visited = set()
        self.max_depth = max_depth
        self.politeness_delay = {}  # domain → last_crawl_time
    
    def crawl(self):
        while self.queue:
            url, depth = self.queue.popleft()
            if url in self.visited or depth > self.max_depth:
                continue
            
            # Respect robots.txt and crawl delay
            if not self.is_allowed(url):
                continue
            
            content = self.fetch(url)
            self.visited.add(url)
            
            # Extract and store
            text = self.extract_text(content)
            links = self.extract_links(content)
            self.store(url, text)
            
            # Enqueue new URLs
            for link in links:
                if link not in self.visited:
                    self.queue.append((link, depth + 1))

2. Inverted index:

Forward index:  doc_id → [word1, word2, ...]
Inverted index: word → [(doc_id, tf, positions), ...]

Example:
  "python tutorial" query
  → Lookup "python": [(doc1, tf=5, [2,15,89]), (doc3, tf=2, [1,7]), ...]
  → Lookup "tutorial": [(doc1, tf=3, [3,20]), (doc2, tf=8, [1,...]), ...]
  → Intersect: docs containing BOTH terms
  → Score by TF-IDF + PageRank + hundreds of other signals
  → Return top K

3. PageRank (simplified):

python
def pagerank(graph: dict, damping: float = 0.85, iterations: int = 100):
    N = len(graph)
    rank = {node: 1.0 / N for node in graph}
    
    for _ in range(iterations):
        new_rank = {}
        for node in graph:
            incoming_sum = sum(
                rank[src] / len(graph[src])
                for src in graph
                if node in graph[src]
            )
            new_rank[node] = (1 - damping) / N + damping * incoming_sum
        rank = new_rank
    
    return rank

Scale: Google processes ~8.5 billion searches per day. The index covers hundreds of billions of web pages. At this scale, you need distributed indexing (MapReduce-style), content-addressed storage (no duplicate pages indexed twice), and serving from pre-built inverted index shards replicated globally.


Part 3 — Infrastructure and Reliability

11. Design a distributed cache

Clarifying questions: Cache-aside or write-through? What consistency is required between cache and DB? What eviction policy? Size limits?

Cache-aside (lazy loading) — most common pattern:

python
def get_user(user_id: str) -> dict:
    # 1. Check cache
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)
    
    # 2. Cache miss — query DB
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    
    # 3. Populate cache
    redis.setex(
        f"user:{user_id}",
        3600,  # TTL: 1 hour
        json.dumps(user)
    )
    
    return user

Write-through — write to cache AND DB together:

python
def update_user(user_id: str, data: dict):
    # Write to DB first (source of truth)
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    # Write to cache (keep in sync)
    redis.setex(f"user:{user_id}", 3600, json.dumps(data))

Cache eviction policies:

  • LRU (Least Recently Used) — evict the item not accessed for the longest time. Good for temporal locality.
  • LFU (Least Frequently Used) — evict the item accessed fewest times. Good when access frequency is a better predictor than recency.
  • TTL-based — items expire after a fixed time. Simplest; good when staleness has a known threshold.

The thundering herd problem:

When a popular cache key expires, hundreds of concurrent requests see a cache miss and all hammer the database simultaneously.

python
import threading

_locks = {}
_locks_mutex = threading.Lock()

def get_with_mutex(key: str, fetch_fn, ttl: int):
    cached = redis.get(key)
    if cached:
        return json.loads(cached)
    
    # Ensure only one request fetches from DB
    with _locks_mutex:
        if key not in _locks:
            _locks[key] = threading.Lock()
        lock = _locks[key]
    
    with lock:
        # Double-check after acquiring lock
        cached = redis.get(key)
        if cached:
            return json.loads(cached)
        
        value = fetch_fn()
        redis.setex(key, ttl, json.dumps(value))
        return value

Cache stampede via probabilistic early expiration (simpler):

python
import math
import random
import time

def get_with_early_expiration(key: str, fetch_fn, ttl: int, beta: float = 1.0):
    data = redis.get(key)
    if data:
        value, expiry = json.loads(data)
        # Probabilistically refresh before expiry
        remaining = expiry - time.time()
        if remaining > 0:
            jitter = -beta * math.log(random.random())
            if remaining > jitter:
                return value
    
    # Fetch and cache
    value = fetch_fn()
    expiry = time.time() + ttl
    redis.setex(key, ttl, json.dumps([value, expiry]))
    return value

12. Design a load balancer

Clarifying questions: L4 (TCP) or L7 (HTTP)? What algorithms? Health checks? Session affinity needed?

Load balancing algorithms:

| Algorithm | How it works | When to use |

|---|---|---|

| Round Robin | Request N goes to server N mod S | Homogeneous servers, stateless |

| Weighted Round Robin | Servers have weights proportional to capacity | Heterogeneous server fleet |

| Least Connections | Next request → server with fewest active connections | Long-lived connections (WebSockets) |

| IP Hash | Hash(client_IP) mod S | Session affinity needed |

| Random with two choices | Pick 2 random servers, send to less loaded one | Good performance, simple implementation |

Least connections implementation:

python
import heapq
import threading

class LoadBalancer:
    def __init__(self, servers: list):
        # Min-heap: (active_connections, server_id)
        self.heap = [(0, s) for s in servers]
        heapq.heapify(self.heap)
        self.lock = threading.Lock()
    
    def get_server(self) -> str:
        with self.lock:
            count, server = heapq.heappop(self.heap)
            heapq.heappush(self.heap, (count + 1, server))
            return server
    
    def release(self, server: str):
        with self.lock:
            # Decrement this server's count
            for i, (count, s) in enumerate(self.heap):
                if s == server:
                    self.heap[i] = (count - 1, s)
                    heapq.heapify(self.heap)
                    break

Health checks:

python
import asyncio
import aiohttp

class HealthChecker:
    def __init__(self, servers: list, interval: int = 10):
        self.servers = servers
        self.healthy = set(servers)
        self.interval = interval
    
    async def check_server(self, server: str):
        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(
                    f"http://{server}/health",
                    timeout=aiohttp.ClientTimeout(total=2)
                ) as resp:
                    if resp.status == 200:
                        self.healthy.add(server)
                    else:
                        self.healthy.discard(server)
        except Exception:
            self.healthy.discard(server)
    
    async def run(self):
        while True:
            await asyncio.gather(*[self.check_server(s) for s in self.servers])
            await asyncio.sleep(self.interval)

13. Design a distributed ID generator (like Snowflake)

Requirements: globally unique, time-sortable, high throughput (100K/sec), no single point of failure.

Snowflake layout:

63 bits total (signed int64, but using as unsigned)

[1 bit: always 0][41 bits: ms timestamp][10 bits: machine ID][12 bits: sequence]

Timestamp: 41 bits = 2^41 ms = ~69 years from epoch
Machine ID: 10 bits = 1024 machines max
Sequence: 12 bits = 4096 IDs per millisecond per machine
Total: 4096 × 1024 machines × 1000 ms = ~4.2 billion IDs/sec
python
import time
import threading

class SnowflakeGenerator:
    def __init__(self, machine_id: int, epoch: int = 1609459200000):  # 2021-01-01
        assert 0 <= machine_id < 1024
        self.machine_id = machine_id
        self.epoch = epoch
        self.sequence = 0
        self.last_ms = -1
        self.lock = threading.Lock()
    
    def next_id(self) -> int:
        with self.lock:
            ms = int(time.time() * 1000) - self.epoch
            
            if ms == self.last_ms:
                self.sequence = (self.sequence + 1) & 0xFFF  # 12-bit mask
                if self.sequence == 0:
                    # Sequence exhausted — wait for next millisecond
                    while ms <= self.last_ms:
                        ms = int(time.time() * 1000) - self.epoch
            else:
                self.sequence = 0
            
            self.last_ms = ms
            
            return (
                (ms << 22) |
                (self.machine_id << 12) |
                self.sequence
            )

gen = SnowflakeGenerator(machine_id=42)
id1 = gen.next_id()
id2 = gen.next_id()
assert id1 < id2  # time-sortable

How machine IDs are assigned:

  • Zookeeper: each node claims a sequential node ID on startup (classic Twitter approach)
  • Environment variables: set MACHINE_ID in Kubernetes deployment spec, each pod gets unique value
  • IP-based: hash last two octets of IP

14. Design a CDN (Content Delivery Network)

Core idea: bring content physically closer to users by caching it at geographically distributed "edge" nodes.

Cache hierarchy:

User (Brazil)
  ↓ DNS lookup → Anycast routes to nearest edge
Edge PoP (São Paulo)
  ↓ cache hit? → return immediately (< 5ms latency)
  ↓ cache miss
Regional Cache (US-East)
  ↓ cache hit? → return + populate edge
  ↓ cache miss
Origin Server (your datacenter)
  → return content + populate regional cache + edge cache

Cache key design:

The cache key determines what counts as the same resource:

Default: method + URL → GET https://cdn.example.com/image.jpg

But be careful with:
- Query strings: is /image.jpg?v=1 the same as /image.jpg?v=2? Usually not.
- Cookies: never include session cookies in cache key (every user gets different content)
- Accept-Encoding: gzip vs. brotli may be served differently
- Vary header: tells CDN to vary cache by specific request headers

Cache invalidation (the hardest problem in CS):

Three strategies:

  1. 1TTL expiration — just wait. Simple but slow for time-sensitive updates.
  2. 2URL versioning — /static/app.v2.js instead of /static/app.js. Perfect for immutable assets. Deploy new version = new URL = automatic cache miss everywhere.
  3. 3Purge API — explicitly tell the CDN to remove a specific URL from all edge caches. Fast but adds operational complexity.

Best practice: immutable assets (JS/CSS with content hash in filename) + very long TTL (1 year) + short TTL (60s) for mutable resources like HTML pages.


15. Design a search autocomplete system

Clarifying questions: How fast must suggestions appear (latency SLA)? How many DAU? Top K suggestions? Personalized or global?

The trie approach:

python
class TrieNode:
    def __init__(self):
        self.children: dict = {}
        self.top_searches: list = []  # pre-computed top-K for this prefix
        self.is_end: bool = False

class AutocompleteService:
    def __init__(self, k: int = 5):
        self.root = TrieNode()
        self.k = k
    
    def insert(self, word: str, frequency: int):
        node = self.root
        for char in word.lower():
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
            # Update top-K at each prefix node
            node.top_searches = self._merge_top_k(
                node.top_searches, (word, frequency)
            )
        node.is_end = True
    
    def search(self, prefix: str) -> list:
        node = self.root
        for char in prefix.lower():
            if char not in node.children:
                return []
            node = node.children[char]
        return [word for word, _ in node.top_searches]
    
    def _merge_top_k(self, current: list, new_entry: tuple) -> list:
        current.append(new_entry)
        current.sort(key=lambda x: -x[1])
        return current[:self.k]

At scale: the trie is rebuilt offline (every hour) from search log aggregations. The current trie is served from memory (read-only; updates are atomic trie swaps, not in-place mutations).

Redis-based autocomplete (simpler, production-friendly):

python
# Offline job: compute top queries per prefix
def build_index(top_queries: list):
    pipe = r.pipeline()
    for query, score in top_queries:
        for i in range(1, len(query) + 1):
            prefix = query[:i]
            pipe.zadd(f"autocomplete:{prefix}", {query: score})
    pipe.execute()

# Real-time lookup: < 1ms
def get_suggestions(prefix: str, k: int = 5) -> list:
    return r.zrevrange(f"autocomplete:{prefix}", 0, k - 1)

Part 4 — Data-Intensive Systems

16. Design a notification system

Clarifying questions: Push (mobile), email, SMS, or in-app? Volume? Guaranteed delivery? User preferences/opt-outs?

Architecture:

Event Source (any service)
  → Kafka topic: "notification-events"
  → Notification Service (fan-out per channel)
      → Email Worker → SendGrid / SES
      → Push Worker → FCM (Android) / APNs (iOS)
      → SMS Worker → Twilio
      → In-App Worker → WebSocket / SSE

Preference and opt-out enforcement:

sql
CREATE TABLE notification_preferences (
    user_id     BIGINT NOT NULL,
    channel     VARCHAR(20) NOT NULL,  -- 'email', 'push', 'sms'
    event_type  VARCHAR(50) NOT NULL,  -- 'new_message', 'payment', etc.
    enabled     BOOLEAN DEFAULT TRUE,
    PRIMARY KEY (user_id, channel, event_type)
);

Push notification delivery with retry:

python
import firebase_admin
from firebase_admin import messaging
import time

def send_push_with_retry(token: str, title: str, body: str, max_retries: int = 3):
    message = messaging.Message(
        notification=messaging.Notification(title=title, body=body),
        token=token
    )
    
    for attempt in range(max_retries):
        try:
            response = messaging.send(message)
            return {"status": "delivered", "message_id": response}
        except messaging.UnregisteredError:
            # Token is invalid — remove from DB
            invalidate_token(token)
            return {"status": "invalid_token"}
        except Exception as e:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # exponential backoff
            else:
                # Move to dead letter queue for manual inspection
                dlq.enqueue({"token": token, "title": title, "body": body, "error": str(e)})
                return {"status": "failed"}

Rate limiting notifications:

Users can be overwhelmed. Implement a per-user digest/throttle: if X notifications in Y minutes, bundle them into a single "You have N new notifications" message.


17. Design a payment system

The cardinal rule: money systems require exactly-once processing. Duplicate charges are business-catastrophic.

Idempotency keys:

python
def process_payment(
    idempotency_key: str,
    user_id: str,
    amount: int,  # in cents
    currency: str
) -> dict:
    # Check if we've already processed this request
    existing = db.get_payment_by_idempotency_key(idempotency_key)
    if existing:
        return existing  # return same response as original
    
    # Acquire distributed lock (prevents race conditions)
    lock_key = f"payment_lock:{idempotency_key}"
    with redis_lock(lock_key, timeout=30):
        # Double-check (another process may have completed while we waited)
        existing = db.get_payment_by_idempotency_key(idempotency_key)
        if existing:
            return existing
        
        # Process the payment
        result = payment_provider.charge(amount, currency)
        
        # Write result atomically
        payment_record = {
            "idempotency_key": idempotency_key,
            "user_id": user_id,
            "amount": amount,
            "currency": currency,
            "status": result.status,
            "provider_id": result.charge_id
        }
        db.insert_payment(payment_record)
        return payment_record

Two-phase commit pattern for distributed payments:

Phase 1 (Prepare):
  Coordinator → Reserve funds (deduct from wallet, mark as "pending")
  Coordinator → Reserve inventory (decrement stock, mark as "held")
  If both OK → proceed to commit

Phase 2 (Commit):
  Coordinator → Confirm wallet deduction → mark as "completed"
  Coordinator → Confirm inventory hold → mark as "sold"
  
  If either fails in Phase 2 → compensating transactions to reverse

Saga pattern (preferred over 2PC for microservices):

OrderService: create_order() → emit "ORDER_CREATED"
PaymentService: on "ORDER_CREATED" → charge() → emit "PAYMENT_COMPLETED" OR "PAYMENT_FAILED"
InventoryService: on "PAYMENT_COMPLETED" → reserve() → emit "INVENTORY_RESERVED"
ShippingService: on "INVENTORY_RESERVED" → schedule_shipment()

Compensations (if any step fails):
  "INVENTORY_FAILED" → PaymentService: refund()
  "PAYMENT_FAILED" → OrderService: cancel_order()

18. Design a hotel/ticket reservation system

The core problem: prevent double-booking (two users reserving the same room/seat simultaneously).

Optimistic locking:

sql
CREATE TABLE reservations (
    id          BIGINT PRIMARY KEY,
    resource_id BIGINT NOT NULL,  -- hotel room, seat, etc.
    user_id     BIGINT NOT NULL,
    check_in    DATE NOT NULL,
    check_out   DATE NOT NULL,
    status      VARCHAR(20) DEFAULT 'confirmed',
    version     INT DEFAULT 0  -- for optimistic locking
);

-- On update, include version in WHERE clause
UPDATE reservations
SET status = 'confirmed', version = version + 1
WHERE id = 123 AND version = 5;  -- fails if someone else already updated

-- If rows_affected = 0 → conflict! Retry or fail.

Database-level uniqueness constraint (prevents double-booking at DB level):

sql
-- For seat reservations: a seat can only be booked once per event
CREATE UNIQUE INDEX idx_no_double_book
ON seat_reservations (event_id, seat_id)
WHERE status != 'cancelled';

-- For hotel rooms: exclude date range overlaps
-- PostgreSQL supports exclusion constraints with daterange
ALTER TABLE room_reservations
ADD CONSTRAINT no_overlap
EXCLUDE USING gist (
    room_id WITH =,
    daterange(check_in, check_out) WITH &&
);

Temporary hold pattern (like Ticketmaster's 10-minute timer):

1. User selects seats → Server creates PENDING reservation (holds seat for 10 min)
2. User completes payment → Server confirms reservation + charges payment
3. If timeout: PENDING reservation expires → seat released back to available pool

Background job: cleanup expired PENDING reservations every minute

19. Design a web crawler at scale

python
# Distributed crawler architecture

# Components:
# 1. URL Frontier: prioritized queue of URLs to crawl
# 2. Fetcher: downloads URLs, respects robots.txt + crawl delay
# 3. Parser: extracts text + outlinks from HTML
# 4. URL Filter: deduplication, normalization
# 5. Content Store: saves raw + parsed content

class DistributedCrawler:
    def __init__(self):
        self.url_frontier = KafkaProducer(topic='urls_to_crawl')
        self.seen_urls = BloomFilter(capacity=10_000_000_000, error_rate=0.001)
        self.robots_cache = {}  # domain → parsed robots.txt
    
    def should_crawl(self, url: str) -> bool:
        # Bloom filter: fast probabilistic deduplication
        if url in self.seen_urls:
            return False
        # Check robots.txt
        domain = extract_domain(url)
        if not self.is_allowed_by_robots(domain, url):
            return False
        return True
    
    def fetch_and_parse(self, url: str):
        # Respect crawl delay
        self.throttle(extract_domain(url))
        
        response = requests.get(url, timeout=5, headers={'User-Agent': 'MyBot/1.0'})
        
        if response.status_code == 200:
            text = extract_text(response.content)
            links = extract_links(response.content, base_url=url)
            
            # Store
            content_store.save(url, text)
            
            # Enqueue new URLs
            for link in links:
                normalized = normalize_url(link)
                if self.should_crawl(normalized):
                    self.seen_urls.add(normalized)
                    self.url_frontier.send(normalized)

Politeness: crawlers must rate-limit per domain. One request per 3-5 seconds per domain is standard. Use a per-domain delay queue.

URL deduplication at 10B URLs: a Bloom filter uses ~12 bits per element at 1% false positive rate. 10B URLs × 12 bits = ~15GB of RAM — fits on a single machine.


20. Design a real-time analytics dashboard

Challenge: process millions of events per second and serve dashboards with low latency.

Lambda architecture:

Raw Events → Kafka
  ↓ (real-time path)
  Stream Processor (Flink/Spark Streaming)
    → Computes rolling 1-min, 5-min, 1-hour aggregates
    → Stores in Redis (query latency: < 10ms)
  ↓ (batch path)
  Batch Processor (Spark, runs every hour)
    → Computes complete historical aggregates
    → Stores in ClickHouse/BigQuery (query latency: 1-10s)

Dashboard query:
  Recent data (last hour) → Redis
  Historical data → ClickHouse
  Merge in application layer

ClickHouse for analytics (why it's fast):

sql
-- ClickHouse: columnar storage, vectorized execution
-- Aggregating 1B rows in 2-3 seconds is typical

SELECT
    toStartOfHour(event_time) AS hour,
    country,
    count() AS events,
    uniqExact(user_id) AS unique_users,
    avg(session_duration) AS avg_duration
FROM events
WHERE event_time >= now() - INTERVAL 7 DAY
  AND event_type = 'page_view'
GROUP BY hour, country
ORDER BY hour DESC, events DESC;

-- With proper partitioning (by date) and MergeTree engine,
-- this query scans only the relevant partitions

Part 5 — Advanced and Behavioral System Design

21. Design a distributed lock service (like Zookeeper)

When you need distributed locks: coordinating access to a shared resource across multiple processes/machines (e.g., ensuring only one worker runs a cron job at a time).

Redis-based distributed lock (Redlock algorithm):

python
import redis
import time
import uuid
from contextlib import contextmanager

class RedisLock:
    def __init__(self, redis_client, key: str, timeout: int = 30):
        self.redis = redis_client
        self.key = f"lock:{key}"
        self.timeout = timeout
        self.identifier = str(uuid.uuid4())
    
    def acquire(self) -> bool:
        # SET key value NX PX timeout
        # NX: only set if not exists (atomic)
        # PX: millisecond TTL (prevents deadlock if process dies)
        return self.redis.set(
            self.key,
            self.identifier,
            nx=True,
            px=self.timeout * 1000
        )
    
    def release(self) -> bool:
        # Lua script: only release if we own the lock
        # Prevents accidentally releasing someone else's lock
        script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        return bool(self.redis.eval(script, 1, self.key, self.identifier))
    
    @contextmanager
    def __call__(self):
        acquired = False
        try:
            # Retry acquisition with backoff
            for _ in range(10):
                if self.acquire():
                    acquired = True
                    break
                time.sleep(0.1)
            if not acquired:
                raise TimeoutError(f"Could not acquire lock: {self.key}")
            yield
        finally:
            if acquired:
                self.release()

# Usage:
lock = RedisLock(r, "critical-section")
with lock():
    # Only one process executes this at a time
    process_data()

Zookeeper ephemeral nodes (stronger guarantees):

Zookeeper's ephemeral nodes are automatically deleted when the client session ends (e.g., process crashes). This makes Zookeeper locks safer than Redis locks for truly critical coordination — you don't rely on a TTL to clean up a dead process's lock.


22. Design a file storage system (like Dropbox)

Core challenges: efficient sync, deduplication, versioning, large file support.

Chunked upload with deduplication:

python
import hashlib

CHUNK_SIZE = 4 * 1024 * 1024  # 4MB chunks

def upload_file(file_path: str, user_id: str) -> str:
    chunks = []
    with open(file_path, 'rb') as f:
        while True:
            data = f.read(CHUNK_SIZE)
            if not data:
                break
            
            chunk_hash = hashlib.sha256(data).hexdigest()
            chunks.append(chunk_hash)
            
            # Check if chunk already exists in storage (deduplication)
            if not chunk_store.exists(chunk_hash):
                chunk_store.put(chunk_hash, data)
    
    # Store file manifest: ordered list of chunk hashes
    file_hash = hashlib.sha256(''.join(chunks).encode()).hexdigest()
    manifest = {
        "file_hash": file_hash,
        "chunks": chunks,
        "user_id": user_id,
        "created_at": time.time()
    }
    manifest_store.put(file_hash, manifest)
    return file_hash

Delta sync (only transfer changed chunks):

python
def sync_file(local_path: str, remote_manifest: dict) -> list:
    # Compare local chunks against remote chunks
    # Only upload chunks that differ or are new
    local_chunks = compute_chunks(local_path)
    remote_chunks = set(remote_manifest.get("chunks", []))
    
    chunks_to_upload = [c for c in local_chunks if c not in remote_chunks]
    return chunks_to_upload

Why this is brilliant:

  • If two users upload the same file, you store it once (content-addressed by hash)
  • If a user edits a 1GB document and changes 1 line, you only upload 1 changed 4MB chunk
  • Git uses this exact model (Merkle trees of content hashes)

23. Design a video conferencing system (like Zoom)

The central problem: real-time, low-latency audio/video with N participants.

WebRTC for P2P (2-person calls):

Alice                    Signaling Server              Bob
  | --- offer SDP ----→ |                              |
  |                     | ------- forward offer ----→  |
  |                     | ←------ answer SDP --------- |
  | ←--- forward ans -- |                              |
  |                                                    |
  | ←------------- ICE candidates exchanged ---------- |
  |                                                    |
  | ←===================== Direct P2P ===============→ |
  |                (bypasses servers entirely)         |

SFU (Selective Forwarding Unit) for group calls:

P2P doesn't scale beyond 3-4 participants (each participant would need to upload N-1 streams). Instead, each participant uploads one stream to a central SFU, which forwards individual streams to each participant.

Participant A → SFU → Participant B
Participant A → SFU → Participant C
Participant B → SFU → Participant A
Participant B → SFU → Participant C
(etc.)

Upload: 1 stream per participant
Download: N-1 streams per participant (or simulcast + adaptive forwarding)

Simulcast + adaptive quality:

Each participant sends their video at 3 quality levels (low/medium/high). The SFU selects which quality to forward to each receiver based on their available bandwidth.


24. Design a recommendation system

Collaborative filtering (users who are similar to you liked X):

python
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

class CollaborativeFilter:
    def __init__(self):
        self.user_item_matrix = None
        self.user_similarities = None
    
    def fit(self, interactions: dict):
        # interactions: {user_id: {item_id: rating}}
        users = list(interactions.keys())
        items = list({item for ratings in interactions.values() for item in ratings})
        
        # Build sparse matrix
        matrix = np.zeros((len(users), len(items)))
        user_idx = {u: i for i, u in enumerate(users)}
        item_idx = {item: i for i, item in enumerate(items)}
        
        for user, ratings in interactions.items():
            for item, rating in ratings.items():
                matrix[user_idx[user]][item_idx[item]] = rating
        
        self.user_item_matrix = matrix
        self.user_similarities = cosine_similarity(matrix)
        self.users = users
        self.items = items
        self.user_idx = user_idx
        self.item_idx = item_idx
    
    def recommend(self, user_id: str, n: int = 10) -> list:
        if user_id not in self.user_idx:
            return []  # cold start problem
        
        idx = self.user_idx[user_id]
        similarities = self.user_similarities[idx]
        
        # Weighted average of ratings from similar users
        scores = np.dot(similarities, self.user_item_matrix)
        
        # Exclude items the user has already interacted with
        already_seen = set(k for k, v in 
                          zip(self.items, self.user_item_matrix[idx]) if v > 0)
        
        recommendations = [
            (self.items[i], scores[i])
            for i in np.argsort(-scores)
            if self.items[i] not in already_seen
        ]
        
        return recommendations[:n]

Cold start problem: new users have no history. Mitigations:

  • Ask for explicit preferences on signup ("What topics interest you?")
  • Use demographic/contextual signals (location, device, referral source)
  • Fall back to popularity-based recommendations
  • Use content-based filtering (recommend items similar to items the user viewed, even briefly)

25. Design a fraud detection system

Requirements: real-time decision (< 100ms), high precision (don't block legitimate users), high recall (catch actual fraud).

Feature engineering (the most important part):

python
def compute_features(transaction: dict, user_history: dict) -> dict:
    return {
        # Velocity features
        "tx_count_1h": count_transactions(user_history, hours=1),
        "tx_count_24h": count_transactions(user_history, hours=24),
        "amount_sum_1h": sum_transactions(user_history, hours=1),
        
        # Deviation features
        "amount_vs_avg": transaction["amount"] / (user_history["avg_amount"] + 1),
        "is_new_merchant": transaction["merchant_id"] not in user_history["known_merchants"],
        "is_new_country": transaction["country"] != user_history["home_country"],
        
        # Time features
        "hour_of_day": datetime.fromtimestamp(transaction["ts"]).hour,
        "is_weekend": datetime.fromtimestamp(transaction["ts"]).weekday() >= 5,
        
        # Device/location features
        "device_seen_before": transaction["device_id"] in user_history["known_devices"],
        "ip_reputation_score": ip_reputation_service.score(transaction["ip"]),
        "distance_from_last_tx_km": haversine(
            transaction["location"], 
            user_history["last_location"]
        )
    }

Decision pipeline:

Transaction arrives
  → Feature computation (< 10ms, from Redis feature store)
  → Rule engine (hard rules: block if X, always fast)
  → ML model (gradient boosting, returns fraud probability)
  → Decision:
      probability < 0.1 → ALLOW
      0.1 ≤ probability < 0.7 → STEP UP (require 2FA)
      probability ≥ 0.7 → BLOCK + alert
  → Log decision + features (for model retraining)

The feedback loop:

  • Blocked transactions that were later confirmed as fraud → positive training examples
  • Chargebacks → high-value positive training examples (strong signal)
  • Approved transactions never disputed → negative training examples
  • Retrain model weekly with new data

Part 6 — System Design Anti-Patterns and Senior-Level Topics

26. Design for failure: circuit breakers

The problem: if Service A calls Service B, and Service B is slow (not down, just slow), Service A's threads pile up waiting. Service A runs out of threads and becomes unavailable too. This is a cascading failure.

python
import time
from enum import Enum
from threading import Lock

class CircuitState(Enum):
    CLOSED = "closed"      # Normal operation
    OPEN = "open"          # Rejecting requests (failing fast)
    HALF_OPEN = "half_open" # Testing if service recovered

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60, success_threshold=2):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.success_threshold = success_threshold
        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self.success_count = 0
        self.last_failure_time = None
        self.lock = Lock()
    
    def call(self, func, *args, **kwargs):
        with self.lock:
            if self.state == CircuitState.OPEN:
                if time.time() - self.last_failure_time > self.recovery_timeout:
                    self.state = CircuitState.HALF_OPEN
                    self.success_count = 0
                else:
                    raise Exception("Circuit is OPEN — fast failing")
        
        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise
    
    def _on_success(self):
        with self.lock:
            self.failure_count = 0
            if self.state == CircuitState.HALF_OPEN:
                self.success_count += 1
                if self.success_count >= self.success_threshold:
                    self.state = CircuitState.CLOSED
    
    def _on_failure(self):
        with self.lock:
            self.failure_count += 1
            self.last_failure_time = time.time()
            if self.failure_count >= self.failure_threshold:
                self.state = CircuitState.OPEN

27. Design a data pipeline (ETL at scale)

Requirements: ingest 10TB of raw data daily, transform, and load into an analytics warehouse.

Modern data stack:

Source Systems (Postgres, MySQL, APIs)
  → CDC (Change Data Capture) via Debezium → Kafka
  → OR: batch export → S3 (raw zone)

Transformation:
  → dbt (SQL-based transforms, runs in the warehouse)
  → OR: Spark (for complex Python/Scala transforms)

Storage:
  → S3 (raw) → S3 (transformed) → BigQuery / Snowflake / Redshift

Orchestration:
  → Airflow / Dagster (DAGs, scheduling, retries, monitoring)

Idempotent ETL (critical for correctness on retries):

python
def load_partition(date: str, source: str):
    """
    Idempotent: safe to run multiple times for same date+source.
    Uses REPLACE INTO / MERGE / truncate+reload semantics.
    """
    data = extract(source, date)
    transformed = transform(data)
    
    # Delete existing data for this partition before loading
    db.execute(f"DELETE FROM facts WHERE date = '{date}' AND source = '{source}'")
    
    # Load fresh data
    db.bulk_insert("facts", transformed)
    
    # Record successful run (for monitoring/auditing)
    db.execute(f"""
        INSERT INTO pipeline_runs (date, source, records, run_at)
        VALUES ('{date}', '{source}', {len(transformed)}, NOW())
        ON CONFLICT (date, source) DO UPDATE SET
            records = {len(transformed)},
            run_at = NOW()
    """)

28. Design a multi-region active-active database

The hardest distributed systems problem: you want to write in multiple regions simultaneously and keep data consistent.

The fundamental tension:

  • Users want low-latency writes (write to nearest region)
  • Consistency requires coordination (are you sure region B doesn't have a conflicting write?)
  • CAP theorem: you can have at most 2 of {Consistency, Availability, Partition Tolerance}

Conflict-free replicated data types (CRDTs) for specific use cases:

python
# G-Counter: only supports increment, automatically merges across replicas
class GCounter:
    def __init__(self, node_id: str, num_nodes: int):
        self.node_id = node_id
        self.counts = [0] * num_nodes
        self.node_index = hash(node_id) % num_nodes
    
    def increment(self, amount: int = 1):
        self.counts[self.node_index] += amount
    
    def value(self) -> int:
        return sum(self.counts)
    
    def merge(self, other: 'GCounter'):
        # Take the maximum count at each position
        self.counts = [max(a, b) for a, b in zip(self.counts, other.counts)]

# This CRDT can be incremented on any node and will always converge
# to the correct total across all nodes, eventually.
# Perfect for: page view counts, like counts, inventory decrements.

For most business data: use a primary region with async replication and accept that some operations require coordination across regions. Route writes to the primary, reads to the nearest replica, and accept that reads may be slightly stale.


29. Design a logging and observability platform (like Datadog)

The three pillars of observability:

| Pillar | What it is | Storage | Query pattern |

|---|---|---|---|

| Logs | Discrete events with context | Elasticsearch / ClickHouse | Full-text search, filter |

| Metrics | Numeric time series | Prometheus / InfluxDB / TimescaleDB | Aggregation, range queries |

| Traces | Causally linked spans across services | Jaeger / Zipkin / Tempo | Trace ID lookup, latency analysis |

Structured logging (machine-parseable):

python
import structlog
import time

logger = structlog.get_logger()

def handle_request(request_id: str, user_id: str, endpoint: str):
    start = time.time()
    try:
        result = process(request_id)
        logger.info(
            "request.completed",
            request_id=request_id,
            user_id=user_id,
            endpoint=endpoint,
            duration_ms=(time.time() - start) * 1000,
            status="success"
        )
        return result
    except Exception as e:
        logger.error(
            "request.failed",
            request_id=request_id,
            user_id=user_id,
            endpoint=endpoint,
            duration_ms=(time.time() - start) * 1000,
            error=str(e),
            status="error"
        )
        raise

Distributed tracing with span propagation:

python
from opentelemetry import trace
from opentelemetry.propagate import inject, extract

tracer = trace.get_tracer("my-service")

def call_downstream_service(headers: dict, payload: dict):
    with tracer.start_as_current_span("downstream-call") as span:
        span.set_attribute("http.url", DOWNSTREAM_URL)
        
        # Inject trace context into outgoing headers
        # This propagates the trace ID to the downstream service
        inject(headers)
        
        response = requests.post(DOWNSTREAM_URL, headers=headers, json=payload)
        span.set_attribute("http.status_code", response.status_code)
        
        return response

30. Design a code deployment system (like GitHub Actions / CI/CD pipeline)

Code push to main
  → Trigger: webhook → CI/CD orchestrator
  → Pipeline:
      1. Checkout code (fast, ~5s)
      2. Build (Docker image, ~2-10 min)
      3. Unit + integration tests (parallel shards, ~5-15 min)
      4. Security scan (SAST, dependency audit)
      5. Push image to registry (tagged with commit SHA)
      6. Deploy to staging
      7. Smoke tests on staging
      8. Manual approval gate (for production)
      9. Rolling deploy to production:
           → Deploy to 5% of instances (canary)
           → Monitor error rate + latency for 10 min
           → If OK: roll out to 100%
           → If anomaly: automatic rollback

Canary deployment with automatic rollback:

python
def canary_deploy(image_tag: str, canary_percentage: int = 5):
    # Route X% of traffic to new version
    update_load_balancer_weights(
        stable=100 - canary_percentage,
        canary=canary_percentage
    )
    
    # Monitor for 10 minutes
    baseline_error_rate = get_error_rate(version="stable", window_minutes=60)
    
    for minute in range(10):
        time.sleep(60)
        canary_error_rate = get_error_rate(version="canary", window_minutes=5)
        
        if canary_error_rate > baseline_error_rate * 1.5:
            # Error rate spiked — rollback immediately
            update_load_balancer_weights(stable=100, canary=0)
            alert(f"Canary rollback: error rate {canary_error_rate:.2%} vs baseline {baseline_error_rate:.2%}")
            return "ROLLED_BACK"
    
    # All good — full rollout
    update_load_balancer_weights(stable=0, canary=100)
    return "DEPLOYED"

Part 7 — The Final 10 Questions (Senior+ Level)

31. How do you design for exactly-once delivery in a distributed system?

Exactly-once is extremely hard. At-least-once + idempotent consumers is the practical answer.

python
class IdempotentConsumer:
    def __init__(self):
        self.processed_ids = redis  # persistent deduplication store
    
    def process(self, message_id: str, payload: dict):
        # Check if already processed
        if self.processed_ids.get(f"processed:{message_id}"):
            return  # skip duplicate
        
        # Process (your business logic here)
        result = do_business_logic(payload)
        
        # Atomic: mark as processed + apply result in same transaction
        with db.transaction():
            apply_result(result)
            self.processed_ids.setex(f"processed:{message_id}", 86400, "1")

32. Explain CAP theorem with a real example

CAP: A distributed system can guarantee at most 2 of: Consistency, Availability, Partition Tolerance.

In practice, network partitions happen (cables fail, packets drop). So you must choose between CP or AP.

  • CP (Consistency over Availability): During a partition, refuse requests rather than return stale data. Example: a bank balance system. You'd rather return an error than give someone their old balance that might be wrong.
  • AP (Availability over Consistency): During a partition, keep serving requests (possibly with stale data). Example: a shopping cart. Better to show slightly stale inventory than to show an error page.

Real systems:

  • Zookeeper, HBase → CP
  • Cassandra, DynamoDB (with eventual consistency) → AP
  • Most SQL databases → CP by default

33. How do you design database sharding?

Horizontal sharding: split rows across multiple database instances.

Shard key selection (most important decision):

  • High cardinality (many distinct values)
  • Evenly distributes data and write load
  • Avoids cross-shard queries for common access patterns
python
# Shard by user_id (good: most queries are per-user)
def get_shard(user_id: int, num_shards: int = 16) -> int:
    return user_id % num_shards

# But what if you need to query across users? (e.g., "find all orders from Brazil")
# That requires querying ALL shards and merging results.
# This is the tradeoff: optimize for per-user queries vs. cross-user analytics.

Resharding (when you need more shards):

  • Naively: move half the data from each shard to new shards — massive downtime
  • Better: consistent hashing — only ~1/N of data moves when adding a shard
  • Best: use a virtual shard mapping table (logical shard → physical shard), start with 1024 logical shards, map multiple to each physical shard, remap as you add machines

34. Design a leader election algorithm

Raft leader election (simplified):

python
import random
import time
import threading

class RaftNode:
    def __init__(self, node_id: str, peers: list):
        self.node_id = node_id
        self.peers = peers
        self.current_term = 0
        self.state = "follower"  # follower | candidate | leader
        self.voted_for = None
        self.votes_received = 0
        self.election_timeout = random.uniform(150, 300)  # ms; randomized to avoid split votes
        self.last_heartbeat = time.time()
    
    def start_election(self):
        self.state = "candidate"
        self.current_term += 1
        self.voted_for = self.node_id
        self.votes_received = 1  # vote for self
        
        # Send RequestVote to all peers
        for peer in self.peers:
            vote_granted = self.request_vote(peer, self.current_term)
            if vote_granted:
                self.votes_received += 1
        
        # If majority votes received → become leader
        if self.votes_received > (len(self.peers) + 1) / 2:
            self.become_leader()
    
    def become_leader(self):
        self.state = "leader"
        # Start sending heartbeats to prevent other nodes from starting elections
        self.send_heartbeats()

35. How do you handle the N+1 query problem?

The problem:

python
# BAD: N+1 queries
orders = db.query("SELECT * FROM orders WHERE user_id = ?", user_id)
for order in orders:
    # This issues 1 query per order!
    items = db.query("SELECT * FROM order_items WHERE order_id = ?", order.id)
    order.items = items
# Total: 1 + N queries

The fix:

python
# GOOD: 2 queries total (or 1 with JOIN)
orders = db.query("SELECT * FROM orders WHERE user_id = ?", user_id)
order_ids = [o.id for o in orders]

# Fetch all items in one query
items = db.query("SELECT * FROM order_items WHERE order_id = ANY(?)", order_ids)

# Group items by order_id in Python (no DB roundtrip)
from collections import defaultdict
items_by_order = defaultdict(list)
for item in items:
    items_by_order[item.order_id].append(item)

for order in orders:
    order.items = items_by_order[order.id]

36. Design a feature flag system

python
class FeatureFlagService:
    def __init__(self):
        self.flags = {}  # loaded from DB/config service
    
    def is_enabled(self, flag_name: str, user_id: str = None, context: dict = None) -> bool:
        flag = self.flags.get(flag_name)
        if not flag:
            return False
        
        # Global kill switch
        if not flag.get("enabled"):
            return False
        
        # Percentage rollout
        if "rollout_percentage" in flag and user_id:
            user_hash = int(hashlib.md5(f"{flag_name}:{user_id}".encode()).hexdigest(), 16)
            if (user_hash % 100) >= flag["rollout_percentage"]:
                return False
        
        # User allowlist
        if "allowed_users" in flag:
            if user_id in flag["allowed_users"]:
                return True
        
        # Attribute targeting (e.g., only for premium users)
        if "targeting" in flag and context:
            for rule in flag["targeting"]:
                if not evaluate_rule(rule, context):
                    return False
        
        return True

37. How do you estimate the storage and throughput for a system?

The key numbers to memorize:

| Unit | Scale |

|---|---|

| 1 million requests/day | ~12 requests/second |

| 1 billion requests/day | ~12,000 requests/second |

| 1KB × 1M = 1GB | |

| 1KB × 1B = 1TB | |

| Average tweet: ~280 chars ≈ 400 bytes | |

| Average photo: 200KB (after compression) | |

| Average video minute: 50MB (1080p H.264) | |

Example estimation for Twitter:

  • 300M DAU, each reads 100 tweets/day = 30B tweet reads/day = ~350K read RPS
  • 300M DAU, each posts 0.1 tweets/day = 30M write/day = ~350 write RPS
  • Read:write ratio = 1000:1
  • Tweet size: 400 bytes + metadata ≈ 1KB
  • New storage: 30M × 1KB = 30GB/day = ~11TB/year (just tweet text)

38. How do you design for graceful degradation?

The principle: when a dependency fails, degrade gracefully instead of failing completely.

python
def get_user_recommendations(user_id: str) -> list:
    try:
        # Primary: personalized recommendations from ML service
        return recommendation_service.get(user_id, timeout=200)  # 200ms SLA
    except (TimeoutError, ServiceUnavailableError):
        try:
            # Fallback 1: cached recommendations from last successful call
            cached = redis.get(f"recs:{user_id}")
            if cached:
                return json.loads(cached)
        except Exception:
            pass
        
        try:
            # Fallback 2: trending items (doesn't require user data)
            return trending_service.get_trending(limit=10, timeout=100)
        except Exception:
            pass
        
        # Final fallback: hardcoded popular items (never fails)
        return DEFAULT_POPULAR_ITEMS

    # At no point do we show an error to the user for a non-critical feature

39. Design a job scheduler / task queue (like Celery)

python
import redis
import json
import time
import uuid
from dataclasses import dataclass

@dataclass
class Task:
    task_id: str
    function: str
    args: list
    kwargs: dict
    priority: int = 5  # 1=highest, 10=lowest
    retry_count: int = 0
    max_retries: int = 3
    eta: float = None  # run_at timestamp (for scheduled tasks)

class TaskQueue:
    def __init__(self, redis_client):
        self.r = redis_client
    
    def enqueue(self, function: str, *args, priority: int = 5, delay_seconds: int = 0, **kwargs) -> str:
        task = Task(
            task_id=str(uuid.uuid4()),
            function=function,
            args=list(args),
            kwargs=kwargs,
            priority=priority,
            eta=time.time() + delay_seconds if delay_seconds else None
        )
        
        if task.eta:
            # Delayed task: add to sorted set with eta as score
            self.r.zadd("delayed_tasks", {json.dumps(task.__dict__): task.eta})
        else:
            # Immediate task: add to priority queue
            score = priority * 1e12 + time.time()  # lower priority = higher score
            self.r.zadd(f"queue:p{priority}", {json.dumps(task.__dict__): score})
        
        return task.task_id
    
    def dequeue(self) -> Task:
        # Check delayed tasks first (move ready ones to main queue)
        self._move_ready_delayed_tasks()
        
        # Pop highest-priority task
        for priority in range(1, 11):
            result = self.r.zpopmin(f"queue:p{priority}", count=1)
            if result:
                return Task(**json.loads(result[0][0]))
        return None
    
    def _move_ready_delayed_tasks(self):
        now = time.time()
        ready = self.r.zrangebyscore("delayed_tasks", 0, now)
        if ready:
            pipe = self.r.pipeline()
            for task_json in ready:
                task = Task(**json.loads(task_json))
                pipe.zrem("delayed_tasks", task_json)
                pipe.zadd(f"queue:p{task.priority}", {task_json: task.priority * 1e12 + now})
            pipe.execute()

40. What are the most important questions to ask before designing any system?

This is the meta-question that separates candidates who have done the work from those who have not.

Always ask:

  1. 1Scale: How many daily active users? What is the expected read and write throughput? Now, and in 2 years?
  1. 2Consistency vs. latency: Is it acceptable to show slightly stale data (eventual consistency), or must every read reflect the latest write (strong consistency)? The answer changes the entire architecture.
  1. 3Availability SLA: What is the acceptable downtime? 99.9% uptime = 8.7 hours/year. 99.99% = 52 minutes/year. These require very different designs.
  1. 4Read vs. write ratio: Is this read-heavy (10:1 → invest in caching, read replicas) or write-heavy (1:10 → invest in sharding, write buffering)?
  1. 5Geographic distribution: Is this serving one region or multiple continents? Multi-region adds enormous complexity.
  1. 6Data size and growth: How much data today? Growth rate per month? How long must data be retained?
  1. 7Access patterns: Is data accessed uniformly (random access) or does it follow a power law (20% of content gets 80% of traffic, Zipf distribution)? This determines caching strategy.
  1. 8Latency requirements: What is the P99 latency requirement? 100ms? 10ms? 1ms? These require very different solutions.
  1. 9Failure modes: What happens when the database is down? Should the system degrade gracefully or fail completely? What are the business implications of each failure?
  1. 10Budget and team constraints: Are you a 3-person startup or a 3,000-person company? The correct design for each is completely different. A beautiful microservices architecture is operational debt for a small team.

Cheat Sheet: Picking the Right Database

| Use Case | Database | Why |

|---|---|---|

| General relational data | PostgreSQL | ACID, JSON support, excellent query planner |

| Simple key-value cache | Redis | In-memory, sub-millisecond latency |

| Write-heavy, high availability | Cassandra | Masterless, linear write scale, tunable consistency |

| Time series (metrics, logs) | TimescaleDB / InfluxDB | Optimized for range queries and aggregations by time |

| Full-text search | Elasticsearch | Inverted index, relevance scoring, faceting |

| OLAP analytics | ClickHouse / BigQuery | Columnar storage, vectorized execution |

| Document store | MongoDB | Flexible schema, rich querying, good for nested data |

| Graph queries | Neo4j | Traversal algorithms, relationship-heavy queries |

| Globally distributed | CockroachDB / Spanner | Distributed SQL, external consistency |


Final Advice

System design interviews test your ability to reason under uncertainty. There is no perfect answer.

The candidates who do best are the ones who:

  • Ask clarifying questions before drawing a single box
  • Estimate scale first, so every subsequent decision is grounded in reality
  • Name the tradeoffs explicitly — "I'm choosing X which gives us Y but costs Z"
  • Know their fundamentals cold (consistent hashing, CAP theorem, leader election, fan-out patterns)
  • Have opinions but hold them loosely — if the interviewer pushes back, engage with their concern rather than defending your first idea

The candidates who fail are the ones who:

  • Jump straight to microservices without understanding the requirements
  • Can describe systems they have read about but cannot reason through novel ones
  • Solve for infinite scale when the requirements called for 10,000 users
  • Never acknowledge that their design has weaknesses

Build real systems. Read engineering blogs from Uber, Cloudflare, Discord, Figma, Notion. Study postmortems (they reveal what breaks in production, which is where system design questions come from).

And practice explaining your reasoning out loud. System design is a conversation, not a monologue.

FAQ

What is the most common system design interview question?+

Design a URL shortener is the most commonly asked entry-level system design question. It covers core concepts including unique ID generation (base62 encoding or random codes), key-value storage, caching strategies (Redis), and CDN usage. Interviewers use it because it is scoped enough to finish in 45 minutes but deep enough to reveal whether a candidate understands read:write ratios, cache invalidation, and analytics pipelines.

How long should a system design interview take?+

A typical system design interview is 45 to 60 minutes. Spend roughly 5 minutes clarifying requirements, 5 minutes on scale estimation, 10 minutes on the high-level design, 20 minutes deep-diving on the hardest component, and 5-10 minutes discussing bottlenecks and tradeoffs. If you find yourself still drawing the happy path at the 30-minute mark, you are moving too slowly.

What is the fan-out problem in system design?+

Fan-out describes the write amplification that occurs when one event must trigger updates to many downstream records. The classic example is Twitter: when a user with 10 million followers posts a tweet, you must update up to 10 million timelines. Solutions are fan-out on write (pre-compute timelines immediately, fast reads but expensive writes), fan-out on read (compute timeline at read time, cheap writes but slow reads), or a hybrid that uses write fan-out for regular users and read fan-out for high-follower celebrities.

What is the CAP theorem and why does it matter for system design?+

CAP theorem states that a distributed system can provide at most two of three guarantees: Consistency (every read returns the most recent write), Availability (every request receives a response), and Partition Tolerance (the system continues operating when network partitions occur). Since network partitions are unavoidable in any real distributed system, the practical choice is between CP (consistency over availability) and AP (availability over consistency). Bank balance systems are typically CP; shopping carts and social feeds are typically AP.

How do you prevent double-booking in a reservation system?+

The most reliable approach combines a database-level uniqueness constraint with optimistic locking. For seat reservations, a UNIQUE constraint on (event_id, seat_id) where status != 'cancelled' makes it physically impossible for two rows representing the same seat at the same event to exist simultaneously. For hotel rooms with date ranges, PostgreSQL supports exclusion constraints on daterange types with the && (overlaps) operator. Additionally, use a temporary PENDING hold with a short TTL (e.g., 10 minutes) so users can complete checkout before the reservation is confirmed.

What is consistent hashing and when should you use it?+

Consistent hashing is a technique for distributing data across a ring of nodes such that when a node is added or removed, only approximately 1/N of all keys need to be remapped (compared to remapping all keys with naive modulo hashing). Use it whenever you need to shard data across a dynamic cluster of nodes, such as in a distributed cache, a distributed key-value store, or a database sharding layer. Virtual nodes (assigning each physical node 100-200 points on the ring) are essential for achieving uniform distribution.

What is the difference between a message queue and a message broker?+

A message queue (like SQS or RabbitMQ) stores messages and delivers each message to exactly one consumer — it is point-to-point. A message broker or event streaming platform (like Kafka) stores messages as an ordered log and allows multiple independent consumer groups to read the same messages without interfering with each other. Use a queue for task distribution (only one worker should process a job). Use a broker for event streaming (multiple services need to react to the same event independently).

How do you handle hot partitions or hot keys in a distributed system?+

A hot key is a cache or database key that receives disproportionately high traffic, overwhelming the single node responsible for it. Common mitigations include: (1) key replication — store N copies of the hot key, each with a random suffix, and read from any copy; (2) local caching in each application server — absorb reads before they hit Redis; (3) write coalescing — batch writes to the hot key rather than writing on every event; (4) rethinking the data model — if a counter is updated 100K times per second, consider using Redis HyperLogLog or a probabilistic counter instead of an exact integer.

What is idempotency and why is it critical in payment systems?+

Idempotency means that performing the same operation multiple times produces the same result as performing it once. In payment systems, this is critical because network failures or timeouts can cause clients to retry requests. Without idempotency, a retry can result in a duplicate charge. The standard implementation uses an idempotency key: a unique UUID the client generates for each payment intent. The server stores this key alongside the result. If the same key arrives again (a retry), the server returns the stored result without processing the payment a second time.

How should you structure your answer in a system design interview?+

Follow this seven-step framework: (1) Clarify requirements — ask about scale, consistency, latency SLAs, and geographic distribution before drawing anything; (2) Estimate scale — calculate requests per second, storage needs, and bandwidth; (3) Define the API — what endpoints does the system expose?; (4) High-level design — draw the happy path from client to database; (5) Deep dive — go deep on the hardest component; (6) Identify bottlenecks — where does the system break at 10x scale?; (7) Discuss tradeoffs — every design decision has a cost, name it explicitly.

Related articles

How to Answer Conflict-With-a-Coworker Interview Questions

Learn how to answer conflict-with-a-coworker interview questions with real examples and proven techniques. Stand out in tech and remote job interviews.

How to Answer 'Why Do You Want to Work Here' in Interviews

Discover expert strategies for answering 'why do you want to work here,' tailored for remote tech roles and dollar opportunities. Real, practical interview tips.

Microservices Interview Questions — 35 Deep Answers

35 microservices interview questions for backend and architect roles: service decomposition, inter-service communication, saga pattern, event sourcing, circuit breakers, observability.

Staff Engineer Interview Questions — 30 with Detailed Answers

30 staff engineer and principal engineer interview questions: technical leadership, cross-team influence, system design at scale, architectural decisions, mentoring. Real answers for L6+.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY