InterviewHack.ai
Empezar gratis
Blog/System Design Interview: How to Design Any System (Step-by-Step)

System Design Interview: How to Design Any System (Step-by-Step)

September 16, 2026

system-designbackend

Complete authoritative article on System Design Interviews with 40 numbered Q&A and real code examples

System Design Interview: How to Design Any System (Step-by-Step)

System design interviews are the most open-ended, highest-signal interviews in software engineering hiring. Unlike coding rounds, there is no single correct answer. The interviewer is evaluating how you think, how you communicate trade-offs, and whether you can navigate ambiguity the same way a senior engineer would in production.

This guide covers the complete framework, every major component you need to know, and 40 real interview questions with detailed answers and working code. Read it end to end before your next loop.


The Universal Framework: 6 Steps for Any System Design Question

Before diving into specific questions, internalize this framework. Apply it to every question, every time.

Step 1 — Clarify Requirements (5 minutes)

Never start drawing boxes. Start by asking questions. The interviewer will give you a vague prompt on purpose.

Axes to clarify:

  • Functional requirements: What does the system do? What are the core user actions?
  • Scale: How many users? Daily active users? Requests per second?
  • Consistency vs. availability: Can users see stale data? What happens during a partition?
  • Read/write ratio: Is this read-heavy (Twitter feed), write-heavy (logging), or balanced?
  • Latency SLA: p99 < 100ms? Real-time? Batch is OK?
  • Durability: What data can we afford to lose? (Usually: none)
  • Geography: Single region? Multi-region? Global CDN needed?

Step 2 — Estimate Scale (5 minutes)

Back-of-envelope math shows the interviewer you understand capacity. It also drives every architectural decision.

Key numbers to know cold:

  • 1 million DAU × 10 requests/day = ~115 RPS average, ~1,150 RPS peak (10× factor)
  • 1 byte = 8 bits; 1 KB = 1,000 bytes; 1 GB = 10^9 bytes
  • SSD random read: ~100 µs; network round-trip same DC: ~0.5 ms; cross-region: ~100 ms
  • MySQL on decent hardware: ~5,000–10,000 writes/sec; ~50,000 reads/sec
  • Redis: ~100,000–1,000,000 ops/sec
  • Kafka: millions of messages/sec per broker

Step 3 — Define the API (5 minutes)

Write out the public-facing API before designing internals. This anchors the conversation.

POST /tweets          { user_id, content, media_ids[] }  → tweet_id
GET  /feed/{user_id}  ?limit=20&cursor=<token>           → Tweet[]
GET  /tweet/{id}                                         → Tweet
DELETE /tweet/{id}                                       → 204

Step 4 — High-Level Design (10 minutes)

Draw the major components. At this stage: clients, load balancer, API servers, databases, caches, queues, CDN. No implementation details yet — just data flow.

Step 5 — Deep Dive (15 minutes)

The interviewer will steer you to the hard part. Common deep-dives: database schema, sharding strategy, cache invalidation, fan-out on write vs. read, consistency model, failure handling.

Step 6 — Identify Bottlenecks and Trade-offs (5 minutes)

Every design has weaknesses. Name them before the interviewer does. Discuss what you would change with more time or different constraints.


Core Building Blocks (Know These Cold)

Load Balancers

  • L4 (TCP): faster, no HTTP awareness — use for non-HTTP or ultra-low latency
  • L7 (HTTP): content-based routing, SSL termination, rate limiting — use almost everywhere
  • Algorithms: Round Robin, Least Connections, IP Hash (sticky sessions), Consistent Hash

Databases

| Type | When to use | Examples |

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

| Relational (RDBMS) | Transactions, complex joins, strong consistency | PostgreSQL, MySQL |

| Document | Flexible schema, nested objects | MongoDB, Firestore |

| Wide-column | Write-heavy, time-series, massive scale | Cassandra, HBase |

| Key-value | Cache, sessions, simple lookups | Redis, DynamoDB |

| Graph | Relationship traversals | Neo4j, Amazon Neptune |

| Search | Full-text, faceted search | Elasticsearch, Meilisearch |

Caching

  • Cache-aside (lazy loading): App checks cache → miss → loads from DB → writes to cache. Simple, handles cold starts well.
  • Write-through: Write to cache and DB simultaneously. Reads always hit cache. More consistent, more writes.
  • Write-behind (write-back): Write to cache, async flush to DB. Fastest writes, risk of data loss.
  • Read-through: Cache sits in front of DB, handles its own misses.

Cache eviction: LRU (most common), LFU, FIFO, TTL-based.

Message Queues

  • Decouple producers from consumers
  • Absorb traffic spikes (buffering)
  • Enable async processing and retry logic
  • Kafka: ordered, replayable, high throughput — use for event streaming, audit logs
  • RabbitMQ/SQS: task queues, at-least-once delivery, simpler semantics

CDN

  • Serves static assets (JS, CSS, images, video) from edge nodes near users
  • Can also cache dynamic API responses with short TTLs
  • Push CDN: you upload assets explicitly
  • Pull CDN: first request goes to origin, subsequent requests hit cache

Consistent Hashing

Used to distribute load across nodes (caches, DB shards) with minimal redistribution when nodes are added/removed.

python
import hashlib
import bisect

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

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

    def remove_node(self, node):
        for i in range(self.replicas):
            key = self._hash(f"{node}:{i}")
            self.ring.pop(key, None)
            idx = bisect.bisect_left(self.sorted_keys, key)
            if idx < len(self.sorted_keys) and self.sorted_keys[idx] == key:
                self.sorted_keys.pop(idx)

    def get_node(self, key):
        if not self.ring:
            return None
        h = self._hash(key)
        idx = bisect.bisect(self.sorted_keys, h) % len(self.sorted_keys)
        return self.ring[self.sorted_keys[idx]]

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

40 System Design Interview Questions — Detailed Answers


1. How do you approach a system design interview from scratch?

Use the 6-step framework: clarify requirements, estimate scale, define API, high-level design, deep dive, trade-offs. Never skip requirements clarification — many candidates dive into drawing boxes and solve the wrong problem. State your assumptions out loud. The interviewer wants to hear your reasoning, not just your conclusions.

Common mistake: Starting with implementation details (database choice, specific algorithms) before establishing what the system must do and at what scale.


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

Requirements:

  • Shorten a URL, return a short code (e.g., sht.ly/xK3m)
  • Redirect users from short URL to original
  • 100M URLs shortened per day; 10B redirects per day

Scale estimation:

  • Write: 100M/day = ~1,160 writes/sec
  • Read: 10B/day = ~115,700 reads/sec (100:1 read/write ratio)
  • Storage: 100M × 500 bytes average URL = 50 GB/day → ~18 TB/year

API:

POST /shorten    { long_url, custom_alias?, ttl_days? } → { short_code }
GET  /{code}     → 301/302 redirect to long_url

Short code generation:

Option A — Random base62 (recommended):

python
import secrets
import string

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

def generate_code(length=7) -> str:
    # 62^7 = 3.5 trillion unique codes
    return ''.join(secrets.choice(ALPHABET) for _ in range(length))

Option B — Counter + base62 encode:

python
def encode_base62(num: int) -> str:
    ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    if num == 0:
        return ALPHABET[0]
    result = []
    while num:
        result.append(ALPHABET[num % 62])
        num //= 62
    return ''.join(reversed(result))

Database schema:

sql
CREATE TABLE urls (
    short_code   CHAR(7)      PRIMARY KEY,
    long_url     TEXT         NOT NULL,
    user_id      BIGINT,
    created_at   TIMESTAMPTZ  NOT NULL DEFAULT now(),
    expires_at   TIMESTAMPTZ,
    click_count  BIGINT       NOT NULL DEFAULT 0
);
CREATE INDEX ON urls (long_url);  -- for dedup check

Architecture:

  • Redis cache: short_code → long_url (99%+ of reads served from cache, TTL matches URL expiry)
  • PostgreSQL for persistent storage
  • Use 301 (permanent) redirect for SEO; use 302 (temporary) if you need click analytics (302 forces browser to re-request, letting you count clicks)
  • For high write throughput, use a distributed counter (Snowflake ID) + base62 to avoid UUID collisions

Trade-offs:

  • Random codes risk collisions at scale → check-on-insert with retry or use atomic counter
  • Custom aliases need a separate uniqueness check
  • Analytics (click counts) should be async (write to Kafka, aggregate separately) — don't block the redirect path

3. What is CAP theorem and how does it affect system design?

CAP theorem states a distributed system can guarantee at most two of three properties simultaneously:

  • Consistency: Every read returns the most recent write or an error
  • Availability: Every request receives a response (not necessarily up-to-date)
  • Partition tolerance: System continues operating despite network partitions

Network partitions are unavoidable in distributed systems, so you always choose between CP (consistency + partition tolerance) or AP (availability + partition tolerance).

CP systems (choose consistency over availability):

  • HBase, Zookeeper, etcd
  • Use when: financial transactions, inventory management, anything where stale data causes real harm

AP systems (choose availability over consistency):

  • Cassandra, CouchDB, DynamoDB (eventually consistent mode)
  • Use when: social feeds, product catalogs, recommendations — users tolerate slightly stale data

In practice: Most systems allow you to tune the trade-off. Cassandra's quorum reads/writes let you slide between AP and CP:

python
# Strong consistency: R + W > N (e.g., N=3, W=2, R=2)
session.execute(
    SimpleStatement(query, consistency_level=ConsistencyLevel.QUORUM)
)

# Eventual consistency: faster, less durable
session.execute(
    SimpleStatement(query, consistency_level=ConsistencyLevel.ONE)
)

PACELC is a more practical extension: even without a partition, there is a trade-off between latency and consistency (E: else, L: latency, C: consistency).


4. Design a key-value store (like Redis or DynamoDB)

Requirements: Get(key), Put(key, value), Delete(key). 1 billion keys, <10ms p99.

Core data structure — an in-memory hash map with a Write-Ahead Log (WAL) for durability:

python
import threading
import json
from pathlib import Path

class KVStore:
    def __init__(self, wal_path="wal.log"):
        self._store: dict = {}
        self._lock = threading.RWLock() if hasattr(threading, 'RWLock') else threading.Lock()
        self._wal = open(wal_path, "a")
        self._replay_wal(wal_path)

    def get(self, key: str):
        with self._lock:
            return self._store.get(key)

    def put(self, key: str, value):
        with self._lock:
            self._append_wal("PUT", key, value)
            self._store[key] = value

    def delete(self, key: str):
        with self._lock:
            self._append_wal("DEL", key, None)
            self._store.pop(key, None)

    def _append_wal(self, op, key, value):
        entry = json.dumps({"op": op, "key": key, "value": value}) + "\n"
        self._wal.write(entry)
        self._wal.flush()  # fsync in production

    def _replay_wal(self, path):
        try:
            for line in Path(path).read_text().splitlines():
                entry = json.loads(line)
                if entry["op"] == "PUT":
                    self._store[entry["key"]] = entry["value"]
                elif entry["op"] == "DEL":
                    self._store.pop(entry["key"], None)
        except FileNotFoundError:
            pass

Scaling to billions of keys:

  • Shard by key hash across N nodes (consistent hashing)
  • Replication: leader-follower (strong consistency) or leaderless (availability)
  • Compaction: periodically merge WAL into a snapshot (LSM tree pattern)

LSM Tree (used by LevelDB, RocksDB, Cassandra):

  • Writes go to in-memory memtable → flushed to immutable SSTables on disk
  • Reads check memtable, then SSTables (newest to oldest)
  • Background compaction merges and garbage-collects SSTables

B-Tree (used by PostgreSQL, MySQL, InnoDB):

  • Reads are O(log n), random I/O
  • Better for read-heavy workloads with complex queries
  • Updates in-place (no compaction overhead)

5. Design Twitter's home feed (news feed)

Requirements: Users follow other users. Show posts from followed users, reverse-chronological, paginated.

Scale: 500M users, 200M DAU, ~500M tweets/day, home timeline reads ~10× writes.

The core problem — fan-out: when user A posts, their 1M followers all need to see it. Two approaches:

Fan-out on write (push model):

  • On tweet creation, write to each follower's feed cache
  • Read is O(1) — just read from pre-computed feed
  • Write amplification: 1 tweet × 1M followers = 1M cache writes
  • Works for users with normal follower counts (<10K)

Fan-out on read (pull model):

  • On feed request, fetch recent tweets from each followed user, merge, sort
  • No write amplification
  • Expensive reads for users following many accounts
  • Works for celebrity reads (you follow 1K people, merge 1K tweet lists)

Hybrid model (what Twitter actually uses):

  • Regular users (<~10K followers): fan-out on write → pre-computed feed in Redis
  • Celebrities (>~10K followers): fan-out on read, injected at read time
python
# Feed write path (simplified)
def handle_new_tweet(tweet: Tweet, follower_ids: list[int]):
    # Store tweet
    tweet_store.put(tweet.id, tweet)

    # Fan out to regular followers
    regular_followers = [
        uid for uid in follower_ids
        if follower_count[uid] < 10_000
    ]
    for follower_id in regular_followers:
        redis.lpush(f"feed:{follower_id}", tweet.id)
        redis.ltrim(f"feed:{follower_id}", 0, 800)  # keep last 800

# Feed read path
def get_feed(user_id: int, cursor: str, limit: int = 20) -> list[Tweet]:
    # Get pre-computed feed from cache
    cached_ids = redis.lrange(f"feed:{user_id}", 0, limit - 1)

    # Inject tweets from celebrities the user follows
    celebrity_ids = get_celebrity_followees(user_id)
    celebrity_tweets = fetch_recent_tweets(celebrity_ids, since=cursor)

    # Merge and sort
    all_ids = merge_sorted([cached_ids, [t.id for t in celebrity_tweets]])
    return [tweet_store.get(tid) for tid in all_ids[:limit]]

Storage:

  • Tweet content: distributed object store (Cassandra or DynamoDB)
  • Feed lists: Redis sorted sets (score = timestamp)
  • Follow graph: separate graph service or wide-column DB

6. How does consistent hashing work and when do you use it?

Standard modular hashing (hash(key) % N) breaks when you add or remove a node — nearly all keys get remapped to different nodes, causing a cache stampede.

Consistent hashing maps both keys and nodes onto a circular ring (0 to 2^32). A key is assigned to the first node clockwise from its position on the ring. Adding/removing a node only remaps keys in one arc segment (~1/N of total keys).

Virtual nodes (vnodes): Each physical node gets mapped to multiple positions on the ring (e.g., 150 virtual nodes per physical node). This ensures uniform distribution even when nodes have different capacities.

python
# From the ConsistentHashRing implementation above:
# Adding 1 node to a 3-node ring:
# Without vnodes: ~33% of keys remapped
# With 150 vnodes: still ~25% remapped (1/4 of ring), but distribution is uniform

ring = ConsistentHashRing(["cache-1", "cache-2", "cache-3"], replicas=150)

# Route request to correct cache node
node = ring.get_node("user:12345")
cache_client = cache_pool[node]
result = cache_client.get("user:12345")

Use consistent hashing when: sharding a distributed cache, distributing load across database shards, partitioning a Kafka topic across consumers.


7. Design a rate limiter

Requirements: Limit requests per user/IP. Support different limits per endpoint. Distributed (multiple API servers share state).

Algorithms:

Token bucket (recommended for most cases):

  • Each user has a bucket that fills at a fixed rate
  • Each request consumes one token
  • Allows bursts up to bucket capacity
python
import time
import redis

class TokenBucketRateLimiter:
    def __init__(self, redis_client, capacity: int, refill_rate: float):
        self.redis = redis_client
        self.capacity = capacity
        self.refill_rate = refill_rate  # tokens per second

    def allow(self, user_id: str) -> bool:
        key = f"rate_limit:{user_id}"
        now = time.time()

        pipe = self.redis.pipeline()
        pipe.hgetall(key)
        results = pipe.execute()
        data = results[0]

        if data:
            tokens = float(data[b"tokens"])
            last_refill = float(data[b"last_refill"])
            elapsed = now - last_refill
            tokens = min(self.capacity, tokens + elapsed * self.refill_rate)
        else:
            tokens = self.capacity
            last_refill = now

        if tokens < 1:
            return False

        pipe = self.redis.pipeline()
        pipe.hset(key, mapping={"tokens": tokens - 1, "last_refill": now})
        pipe.expire(key, int(self.capacity / self.refill_rate) + 10)
        pipe.execute()
        return True

Sliding window log: Accurate but memory-intensive (stores timestamp of every request).

Sliding window counter: Approximate but memory-efficient. Uses two fixed windows (current + previous) weighted by overlap.

python
def sliding_window_allow(redis_client, user_id: str, limit: int, window_sec: int) -> bool:
    now = int(time.time())
    current_window = now // window_sec
    prev_window = current_window - 1

    current_key = f"rl:{user_id}:{current_window}"
    prev_key = f"rl:{user_id}:{prev_window}"

    pipe = redis_client.pipeline()
    pipe.get(current_key)
    pipe.get(prev_key)
    current_count, prev_count = [int(x or 0) for x in pipe.execute()]

    # Weight prev window by how much it overlaps with current sliding window
    elapsed_in_window = now % window_sec
    weighted_prev = prev_count * (1 - elapsed_in_window / window_sec)
    estimated_count = weighted_prev + current_count

    if estimated_count >= limit:
        return False

    pipe = redis_client.pipeline()
    pipe.incr(current_key)
    pipe.expire(current_key, window_sec * 2)
    pipe.execute()
    return True

Distributed implementation: Use Redis with Lua scripts for atomic check-and-increment (avoids race conditions across multiple API servers).


8. Design a distributed cache

Requirements: Sub-millisecond reads, horizontal scaling, cache eviction, replication.

Partitioning: Consistent hashing across N cache nodes. Client-side routing (each app server knows the topology) vs. proxy-based routing (like Redis Cluster's gossip protocol).

Replication: Leader-follower per shard. Writes go to leader, replicated async to followers. Leader failure → follower election (Raft or Zookeeper-based).

Eviction policies:

LRU  — evict least recently used (most common)
LFU  — evict least frequently used (better for hot-key patterns)
TTL  — expire by time (simplest, use for session data)
ARC  — Adaptive Replacement Cache, tracks both recency and frequency

Cache stampede / thundering herd — when a popular key expires and hundreds of requests simultaneously miss and all query the DB:

python
import threading
import time

# Solution 1: Probabilistic early expiration
def get_with_early_expiry(cache, db, key, ttl, beta=1.0):
    cached = cache.get_with_ttl(key)  # returns (value, remaining_ttl)
    if cached:
        value, remaining_ttl = cached
        # Probabilistically recompute before expiry
        if -beta * math.log(random.random()) > remaining_ttl:
            value = db.get(key)
            cache.set(key, value, ttl)
        return value
    value = db.get(key)
    cache.set(key, value, ttl)
    return value

# Solution 2: Mutex lock on cache miss
_locks = {}
_lock_meta = threading.Lock()

def get_with_mutex(cache, db, key, ttl):
    value = cache.get(key)
    if value:
        return value

    with _lock_meta:
        if key not in _locks:
            _locks[key] = threading.Lock()
        lock = _locks[key]

    with lock:
        # Check again after acquiring lock
        value = cache.get(key)
        if value:
            return value
        value = db.get(key)
        cache.set(key, value, ttl)
        return value

Hot key problem: One key gets millions of requests/sec. Solutions:

  1. 1Local in-process cache (L1 cache in front of Redis)
  2. 2Key replication: hot_key → hot_key_shard_0...N, randomly pick shard on read
  3. 3Read replicas dedicated to that shard

9. Design a web crawler

Requirements: Crawl 1 billion pages, store content, respect robots.txt, avoid duplicate crawls.

Components:

  1. 1URL Frontier (priority queue of URLs to crawl)
  2. 2Fetcher (HTTP client pool)
  3. 3Parser (extract links + content)
  4. 4Deduplication filter
  5. 5Storage (raw HTML + extracted content)

URL deduplication — use a Bloom filter for space-efficient membership check:

python
from bitarray import bitarray
import mmh3  # MurmurHash3

class BloomFilter:
    def __init__(self, size: int, hash_count: int):
        self.size = size
        self.hash_count = hash_count
        self.bit_array = bitarray(size)
        self.bit_array.setall(0)

    def add(self, item: str):
        for seed in range(self.hash_count):
            idx = mmh3.hash(item, seed) % self.size
            self.bit_array[idx] = 1

    def __contains__(self, item: str) -> bool:
        return all(
            self.bit_array[mmh3.hash(item, seed) % self.size]
            for seed in range(self.hash_count)
        )

# 1 billion URLs, ~1% false positive rate: ~10 bits/element = 1.25 GB
url_filter = BloomFilter(size=10_000_000_000, hash_count=7)

Politeness: Each domain gets its own crawl queue with a minimum delay (e.g., 1 req/sec per domain). Use a priority heap sorted by next_crawl_time.

Distributed architecture:

  • URL Frontier: Kafka topic partitioned by domain hash (keeps politeness per domain on one worker)
  • Content store: HDFS or S3 for raw HTML
  • Metadata DB: HBase or Cassandra (URL, crawl timestamp, content hash, HTTP status)

robots.txt: Cache per domain, respect Crawl-delay, honor Disallow rules.


10. Design a notification system

Requirements: Send push, email, and SMS notifications. 100M notifications/day. Delivery guarantees, deduplication, user preferences.

Flow:

Service → Notification Service → Queue → Workers → Third-party providers
python
# Notification schema
{
    "notification_id": "uuid",
    "user_id": 12345,
    "type": "push|email|sms",
    "template_id": "order_shipped",
    "template_vars": {"order_id": "ORD-999", "eta": "tomorrow"},
    "priority": "high|normal|low",
    "dedup_key": "order_shipped:ORD-999:user:12345"
}

Deduplication: Before enqueuing, check dedup_key in Redis with a TTL matching your retry window (e.g., 24h). Idempotent inserts prevent duplicate sends from retries.

Multi-channel fan-out:

python
def dispatch_notification(notification: dict):
    user_prefs = user_prefs_store.get(notification["user_id"])

    channels = []
    if user_prefs.push_enabled and notification["type"] in ("push", "all"):
        channels.append(PushWorker)
    if user_prefs.email_enabled and notification["type"] in ("email", "all"):
        channels.append(EmailWorker)
    if user_prefs.sms_enabled and notification["type"] in ("sms", "urgent"):
        channels.append(SMSWorker)

    for channel in channels:
        queue.enqueue(channel.QUEUE_NAME, notification)

Retry strategy: Exponential backoff with jitter. Failed notifications move to a dead-letter queue (DLQ) for inspection.

python
def retry_delay(attempt: int, base_ms=1000) -> float:
    cap = 32_000  # max 32 seconds
    delay = min(cap, base_ms * (2 ** attempt))
    jitter = delay * 0.2 * random.random()
    return (delay + jitter) / 1000  # in seconds

At-least-once delivery is standard. Ensure your third-party sends are idempotent (most providers accept an idempotency key).


11. How do you design a database schema for a social network?

Core entities: Users, Posts, Follows, Likes, Comments.

sql
-- Users
CREATE TABLE users (
    id          BIGSERIAL PRIMARY KEY,
    username    VARCHAR(50) UNIQUE NOT NULL,
    email       VARCHAR(255) UNIQUE NOT NULL,
    created_at  TIMESTAMPTZ DEFAULT now()
);

-- Posts
CREATE TABLE posts (
    id          BIGSERIAL PRIMARY KEY,
    user_id     BIGINT NOT NULL REFERENCES users(id),
    content     TEXT NOT NULL,
    media_urls  TEXT[],
    created_at  TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX ON posts (user_id, created_at DESC);

-- Follow graph
CREATE TABLE follows (
    follower_id  BIGINT NOT NULL REFERENCES users(id),
    followee_id  BIGINT NOT NULL REFERENCES users(id),
    created_at   TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (follower_id, followee_id)
);
CREATE INDEX ON follows (followee_id);  -- "who follows me" queries

-- Likes (high write volume — consider denormalized counter on posts)
CREATE TABLE likes (
    post_id    BIGINT NOT NULL REFERENCES posts(id),
    user_id    BIGINT NOT NULL REFERENCES users(id),
    created_at TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (post_id, user_id)
);

-- Denormalized counter to avoid COUNT(*) on likes
ALTER TABLE posts ADD COLUMN like_count BIGINT DEFAULT 0;

Scaling follow graph: At high scale, the follows table becomes a bottleneck for feed generation. Move to a graph database (Neo4j) or a specialized follow service backed by Cassandra.

Sharding: Shard posts and follows by user_id. Keep a user's data on the same shard for locality. Cross-shard queries (e.g., feed generation) require scatter-gather.


12. Explain database sharding strategies

Horizontal sharding splits rows across multiple database instances. Three main strategies:

Range-based sharding:

Shard 0: user_id 0–10M
Shard 1: user_id 10M–20M
...
  • Pro: Simple, range queries on shard key are fast
  • Con: Hot spots (newest users are always on last shard)

Hash-based sharding:

python
def get_shard(user_id: int, num_shards: int) -> int:
    return hash(user_id) % num_shards
  • Pro: Uniform distribution
  • Con: Range queries require scatter-gather; resharding is painful (consistent hashing mitigates this)

Directory-based sharding:

  • Lookup service maps key → shard
  • Pro: Flexible, supports heterogeneous shard sizes
  • Con: Lookup service is a bottleneck + SPOF unless highly available

Cross-shard challenges:

  • JOINs: Must be done in application layer
  • Transactions: Use two-phase commit (2PC) or saga pattern
  • Unique IDs: Can't use auto-increment; use Snowflake IDs or UUIDs

Snowflake ID (Twitter's distributed ID generation):

41 bits timestamp | 10 bits machine ID | 12 bits sequence
python
import time

class SnowflakeIDGenerator:
    EPOCH = 1288834974657  # Twitter epoch (Nov 4, 2010)
    MACHINE_ID_BITS = 10
    SEQUENCE_BITS = 12
    MAX_MACHINE_ID = (1 << MACHINE_ID_BITS) - 1
    MAX_SEQUENCE = (1 << SEQUENCE_BITS) - 1

    def __init__(self, machine_id: int):
        assert 0 <= machine_id <= self.MAX_MACHINE_ID
        self.machine_id = machine_id
        self.sequence = 0
        self.last_timestamp = -1

    def generate(self) -> int:
        ts = int(time.time() * 1000) - self.EPOCH
        if ts == self.last_timestamp:
            self.sequence = (self.sequence + 1) & self.MAX_SEQUENCE
            if self.sequence == 0:
                while ts <= self.last_timestamp:
                    ts = int(time.time() * 1000) - self.EPOCH
        else:
            self.sequence = 0
        self.last_timestamp = ts
        return (ts << 22) | (self.machine_id << 12) | self.sequence

13. Design a distributed message queue (like Kafka)

Core concepts:

  • Topic: Named stream of messages
  • Partition: Ordered, immutable sequence of messages. Topics split into N partitions for parallelism.
  • Offset: Position of a message within a partition
  • Consumer group: Set of consumers dividing partitions among themselves

Guarantees:

  • Messages within a partition are ordered
  • At-least-once delivery by default (idempotent consumers needed)
  • Exactly-once via transactions (more overhead)

Storage: Each partition is a segment of append-only log files on disk. Sequential writes are fast (~hundreds of MB/sec).

/data/kafka/
  topic-orders/
    partition-0/
      00000000000000000000.log   (segment 0)
      00000000000000012345.log   (segment 12345)
      00000000000000012345.index

Producer with batching:

python
from confluent_kafka import Producer

producer = Producer({
    "bootstrap.servers": "kafka-1:9092,kafka-2:9092",
    "linger.ms": 5,          # wait up to 5ms to batch messages
    "batch.size": 65536,     # 64KB batch
    "acks": "all",           # wait for all replicas
    "enable.idempotence": True,
})

def send_order_event(order: dict):
    producer.produce(
        topic="orders",
        key=str(order["user_id"]).encode(),  # same user → same partition → ordered
        value=json.dumps(order).encode(),
        callback=delivery_report,
    )
    producer.poll(0)  # trigger callbacks

def delivery_report(err, msg):
    if err:
        logger.error(f"Delivery failed: {err}")

Consumer with manual offset commit:

python
from confluent_kafka import Consumer

consumer = Consumer({
    "bootstrap.servers": "kafka-1:9092",
    "group.id": "order-processor",
    "auto.offset.reset": "earliest",
    "enable.auto.commit": False,  # manual commit for exactly-once processing
})
consumer.subscribe(["orders"])

while True:
    msg = consumer.poll(timeout=1.0)
    if msg is None or msg.error():
        continue
    order = json.loads(msg.value())
    process_order(order)  # idempotent
    consumer.commit(message=msg)  # commit only after successful processing

Replication: Each partition has a leader and N-1 followers. Producers write to leader, followers replicate. If leader dies, a follower with all committed messages becomes the new leader (ISR — In-Sync Replicas).


14. Design a search autocomplete system

Requirements: As user types, return top 10 completions ranked by search frequency. Latency < 100ms.

Trie-based approach (in-memory, single machine):

python
class TrieNode:
    def __init__(self):
        self.children: dict[str, 'TrieNode'] = {}
        self.is_end = False
        self.frequency = 0
        self.top_suggestions: list[tuple[int, str]] = []  # (freq, word), max-heap

class AutocompleteTrie:
    def __init__(self, max_suggestions=10):
        self.root = TrieNode()
        self.max_suggestions = max_suggestions

    def insert(self, word: str, frequency: int):
        node = self.root
        for ch in word.lower():
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_end = True
        node.frequency = frequency
        self._update_suggestions(word, frequency)

    def _update_suggestions(self, word: str, freq: int):
        node = self.root
        for ch in word.lower():
            node = node.children[ch]
            suggestions = node.top_suggestions
            # Keep top-K sorted by frequency (use heap for efficiency)
            if len(suggestions) < self.max_suggestions:
                suggestions.append((freq, word))
                suggestions.sort(reverse=True)
            elif freq > suggestions[-1][0]:
                suggestions[-1] = (freq, word)
                suggestions.sort(reverse=True)

    def search(self, prefix: str) -> list[str]:
        node = self.root
        for ch in prefix.lower():
            if ch not in node.children:
                return []
            node = node.children[ch]
        return [word for _, word in node.top_suggestions]

At scale:

  • Store trie in shared memory (memcached) or serialize to Redis
  • Precompute top-K suggestions per prefix node offline (batch job every hour from search logs)
  • Partition trie by first 2 characters: prefix[:2] → shard ID

Real-time frequency updates: Log searches to Kafka → Flink job aggregates counts → updates trie every hour (full rebuild) or 10 minutes (incremental diff).

Typo tolerance: Use Elasticsearch with edge n-gram tokenizer for fuzzy matching at the cost of higher latency (still < 100ms for most queries).


15. Design a payment system

Requirements: Process payments, handle failures, prevent double charges, support refunds.

The core challenge: Payments require exactly-once semantics. Network failures make this hard — you don't know if the charge succeeded or failed.

Idempotency keys:

python
def charge_card(
    user_id: int,
    amount_cents: int,
    currency: str,
    idempotency_key: str,  # client-generated UUID, stored by caller
) -> PaymentResult:
    # Check if we already processed this request
    existing = db.get_payment_by_idempotency_key(idempotency_key)
    if existing:
        return existing  # return same result, no double charge

    # Create payment record in PENDING state FIRST
    payment_id = db.create_payment(
        user_id=user_id,
        amount=amount_cents,
        currency=currency,
        idempotency_key=idempotency_key,
        status="PENDING",
    )

    try:
        # Call payment provider (Stripe, Adyen, etc.)
        provider_result = stripe.charge(
            amount=amount_cents,
            currency=currency,
            customer_id=user_id,
            idempotency_key=idempotency_key,
        )
        db.update_payment(payment_id, status="SUCCESS", provider_id=provider_result.id)
        return PaymentResult(success=True, payment_id=payment_id)
    except stripe.CardError as e:
        db.update_payment(payment_id, status="FAILED", error=str(e))
        return PaymentResult(success=False, error=str(e))
    except Exception as e:
        # Network timeout — status is UNKNOWN, not FAILED
        db.update_payment(payment_id, status="UNKNOWN", error=str(e))
        # Reconciliation job will resolve this async
        raise

Payment state machine:

PENDING → SUCCESS → REFUNDED
        → FAILED
        → UNKNOWN → (reconciliation) → SUCCESS | FAILED

Double-spend prevention: Database unique constraint on idempotency_key. Any duplicate request returns the stored result.

Reconciliation: A background job periodically queries the payment provider for all UNKNOWN/PENDING payments and updates status. Essential for handling network timeouts.

Ledger pattern: Never update balances in place. Append-only ledger entries:

sql
CREATE TABLE ledger (
    id           BIGSERIAL PRIMARY KEY,
    account_id   BIGINT NOT NULL,
    amount_cents BIGINT NOT NULL,  -- positive = credit, negative = debit
    type         VARCHAR(50) NOT NULL,  -- 'charge', 'refund', 'payout'
    reference_id VARCHAR(255) NOT NULL,
    created_at   TIMESTAMPTZ DEFAULT now()
);
-- Balance = SUM(amount_cents) WHERE account_id = X

16. Design a video streaming service (like YouTube)

Upload pipeline:

User → Upload Service → Raw Storage (S3)
                      → Transcoding Job Queue (Kafka)
                      → Transcoding Workers (FFmpeg)
                      → Multiple resolution outputs (S3: 360p, 720p, 1080p, 4K)
                      → CDN edge nodes

Transcoding at scale:

  • Split video into 5-second GOP (Group of Pictures) chunks
  • Transcode each chunk in parallel across a worker pool
  • Reassemble segments into final output
  • Each resolution is a separate job: 360p/720p/1080p/4K
  • 1 hour of raw video → 2-4 hours of CPU compute → cloud-scale worker fleet reduces to minutes

Adaptive Bitrate Streaming (HLS/DASH):

  • Player requests a manifest file (.m3u8) listing available bitrates
  • Fetches 2–10 second segments from CDN
  • Switches bitrate based on measured bandwidth
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
360p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720
720p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1920x1080
1080p/playlist.m3u8

CDN strategy: Cache segments at edge. Video-on-demand → high cache hit rate (popular videos watched millions of times). Live streaming → segments are never cached, must be served from origin or very short TTL.

Metadata storage:

sql
CREATE TABLE videos (
    id           VARCHAR(11) PRIMARY KEY,  -- YouTube-style ID
    user_id      BIGINT NOT NULL,
    title        TEXT NOT NULL,
    description  TEXT,
    duration_sec INT,
    status       VARCHAR(20) NOT NULL,  -- 'processing', 'ready', 'failed'
    view_count   BIGINT DEFAULT 0,
    created_at   TIMESTAMPTZ DEFAULT now()
);

View counts are eventually consistent — increment in Redis, flush to DB in batches.


17. Design a ride-sharing service (like Uber)

Core challenge: Match riders to nearby drivers in real time. Geospatial queries at scale.

Geohashing: Encode lat/lng as a short string. Adjacent cells share prefixes.

python
import geohash  # python-geohash library

# Driver location update
def update_driver_location(driver_id: int, lat: float, lng: float):
    gh = geohash.encode(lat, lng, precision=6)  # ~1km accuracy
    # Store in Redis geospatial index
    redis.geoadd("active_drivers", lng, lat, driver_id)
    # Also store in geohash bucket for fast proximity queries
    redis.sadd(f"drivers:geohash:{gh}", driver_id)
    redis.expire(f"drivers:geohash:{gh}", 30)  # drivers expire if not updated

# Find nearby drivers
def find_nearby_drivers(lat: float, lng: float, radius_km: float = 5) -> list[int]:
    results = redis.georadius(
        "active_drivers", lng, lat,
        radius_km, unit="km",
        withdist=True, count=20, sort="ASC"
    )
    return [(int(driver_id), dist) for driver_id, dist in results]

Matching algorithm:

  • Find N nearest available drivers
  • Estimated time of arrival (ETA) for each
  • Assign to driver with shortest ETA
  • Reject if ETA > threshold (e.g., 10 min)

Trip state machine:

REQUESTED → ACCEPTED → DRIVER_ARRIVING → IN_PROGRESS → COMPLETED
          → CANCELLED (by rider or driver)
          → NO_DRIVER_FOUND

Surge pricing: Calculate supply/demand ratio per geohash cell. If demand / supply > threshold, apply multiplier. Pre-compute cell-level stats every 5 minutes.


18. Design a distributed lock service

Requirements: Mutual exclusion across distributed services. Fault-tolerant. Avoid deadlocks.

Redis-based distributed lock (Redlock):

python
import uuid
import time
import redis

class DistributedLock:
    def __init__(self, redis_client, key: str, ttl_ms: int = 10000):
        self.redis = redis_client
        self.key = f"lock:{key}"
        self.ttl_ms = ttl_ms
        self.token = str(uuid.uuid4())

    def acquire(self, retry_times=3, retry_delay_ms=200) -> bool:
        for _ in range(retry_times):
            # SET key token NX PX ttl — atomic, only sets if not exists
            result = self.redis.set(
                self.key, self.token,
                nx=True, px=self.ttl_ms
            )
            if result:
                return True
            time.sleep(retry_delay_ms / 1000)
        return False

    def release(self):
        # Lua script ensures we only release our own lock (atomic check-and-delete)
        script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        self.redis.eval(script, 1, self.key, self.token)

    def __enter__(self):
        if not self.acquire():
            raise RuntimeError(f"Could not acquire lock: {self.key}")
        return self

    def __exit__(self, *args):
        self.release()

# Usage
with DistributedLock(redis_client, "order:12345", ttl_ms=5000):
    process_order(12345)

Redlock (multi-node, more fault-tolerant): Acquire lock on N/2+1 Redis nodes. If majority acquired within validity_time = ttl - elapsed - drift, lock is valid. On failure or partial success, release all nodes.

When Redis locks aren't enough: Use Zookeeper ephemeral nodes (lock released automatically on client disconnect) or etcd leases for stronger guarantees.


19. How do you design for high availability (HA)?

HA = eliminating single points of failure (SPOF)

Checklist:

  • Load balancer: at least 2 (active-active or active-passive with VIP failover)
  • App servers: N+1 minimum, auto-scaling group
  • Database: primary + at least one replica; automated failover (e.g., RDS Multi-AZ, Patroni for PostgreSQL)
  • Cache: Redis Sentinel or Redis Cluster
  • Message queue: Kafka with replication factor 3, min.insync.replicas=2

Multi-region:

  • Active-active: both regions serve traffic, data replicated across regions; risk of split-brain
  • Active-passive: one region serves traffic, second is hot standby; simpler but higher failover time

Circuit breaker pattern — stop calling a failing service:

python
from enum import Enum
import time

class CircuitState(Enum):
    CLOSED = "closed"      # normal operation
    OPEN = "open"          # failing, reject all calls
    HALF_OPEN = "half_open"  # test if service recovered

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = CircuitState.CLOSED

    def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
            else:
                raise Exception("Circuit breaker OPEN — service unavailable")

        try:
            result = func(*args, **kwargs)
            if self.state == CircuitState.HALF_OPEN:
                self.reset()
            return result
        except Exception:
            self.failure_count += 1
            self.last_failure_time = time.time()
            if self.failure_count >= self.failure_threshold:
                self.state = CircuitState.OPEN
            raise

    def reset(self):
        self.failure_count = 0
        self.state = CircuitState.CLOSED

Health checks: Liveness (is the process alive?) vs. readiness (is it ready to serve traffic?). Kubernetes distinguishes these. A pod can be alive but not ready (e.g., still warming up cache).


20. Design a file storage system (like Google Drive or Dropbox)

Requirements: Upload/download files, sync across devices, share with other users, version history.

Chunked upload:

  • Split files into 4MB chunks
  • Upload chunks in parallel
  • Resume interrupted uploads from the last successful chunk
  • Deduplication: hash each chunk (SHA-256); if chunk already stored, skip upload
python
import hashlib

def compute_chunk_hash(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()

def upload_file(file_path: str, user_id: int, chunk_size: int = 4 * 1024 * 1024):
    chunks = []
    with open(file_path, "rb") as f:
        while True:
            data = f.read(chunk_size)
            if not data:
                break
            chunk_hash = compute_chunk_hash(data)
            chunks.append(chunk_hash)

            # Only upload if not already stored (content-addressable)
            if not chunk_store.exists(chunk_hash):
                chunk_store.put(chunk_hash, data)

    # Store file metadata: list of chunk hashes in order
    file_id = metadata_store.create_file(
        user_id=user_id,
        filename=os.path.basename(file_path),
        chunk_hashes=chunks,
        size=os.path.getsize(file_path),
    )
    return file_id

Storage: Chunks go to S3 (or equivalent object store) keyed by their hash. This is content-addressable storage — same content stored once regardless of how many users have the same file.

Sync protocol:

  1. 1Client computes local file tree with hashes
  2. 2Delta sync: compare with server state, only transfer changed chunks
  3. 3Server sends change notifications via WebSocket or long-polling
  4. 4Conflict resolution: last-write-wins, or create a "conflicted copy" (Dropbox's approach)

Metadata DB:

sql
CREATE TABLE files (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id      BIGINT NOT NULL,
    parent_id    UUID REFERENCES files(id),  -- folder hierarchy
    name         VARCHAR(255) NOT NULL,
    size_bytes   BIGINT NOT NULL,
    is_folder    BOOLEAN NOT NULL DEFAULT FALSE,
    version      INT NOT NULL DEFAULT 1,
    chunk_hashes TEXT[],  -- ordered list of content-addressable chunk hashes
    created_at   TIMESTAMPTZ DEFAULT now(),
    updated_at   TIMESTAMPTZ DEFAULT now()
);

21. Design a recommendation system

Two main approaches:

Collaborative filtering: "Users like you also liked..."

  • Item-based: find items similar to what user liked (cosine similarity on item vectors)
  • User-based: find similar users, recommend what they liked
  • Matrix factorization (SVD, ALS): decompose user-item matrix into latent factor vectors

Content-based filtering: "Because you liked X (which has these attributes)..."

  • Build item feature vectors (genre, tags, metadata)
  • Build user preference vectors from their history
  • Recommend items with high similarity to user vector

In practice (Netflix/Spotify approach): ensemble both + deep learning.

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

class ItemBasedCF:
    def __init__(self):
        self.item_vectors = {}  # item_id → embedding vector
        self.similarity_matrix = None

    def train(self, interaction_matrix: np.ndarray, item_ids: list[int]):
        # interaction_matrix: users × items, values = implicit feedback (views, clicks)
        # Transpose: items × users
        item_user_matrix = interaction_matrix.T
        self.similarity_matrix = cosine_similarity(item_user_matrix)
        self.item_ids = item_ids

    def recommend(self, user_history: list[int], n: int = 10) -> list[int]:
        if not user_history:
            return self._popular_items(n)

        # Average similarity across all items user interacted with
        history_indices = [self.item_ids.index(i) for i in user_history if i in self.item_ids]
        scores = self.similarity_matrix[history_indices].mean(axis=0)

        # Exclude already-seen items
        for idx in history_indices:
            scores[idx] = -1

        top_indices = np.argsort(scores)[-n:][::-1]
        return [self.item_ids[i] for i in top_indices]

Two-tower neural network (Google's approach for YouTube):

  • User tower: embeddings from user history, demographics
  • Item tower: embeddings from item features
  • Train to maximize dot product for positive (user, item) pairs
  • Serve: pre-compute all item embeddings → approximate nearest neighbor search (FAISS) at query time

22. What is the difference between SQL and NoSQL databases?

| Property | SQL (Relational) | NoSQL |

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

| Schema | Fixed, enforced | Flexible (document) or schema-free |

| Transactions | ACID by default | Often BASE (eventual consistency) |

| Joins | Native, efficient | Application-level or denormalized |

| Scaling | Vertical + read replicas | Horizontal sharding (native) |

| Query flexibility | Arbitrary SQL | Limited to access patterns defined at design time |

| Consistency | Strong by default | Configurable (eventual to strong) |

When to use SQL: Financial data, user accounts, anything requiring transactions or complex queries. PostgreSQL handles surprisingly high scale with proper indexing and connection pooling.

When to use NoSQL:

  • Document (MongoDB): Content management, user profiles, product catalogs with variable attributes
  • Wide-column (Cassandra): Write-heavy time-series, IoT data, chat messages, event logs
  • Key-value (Redis): Session store, cache, leaderboards, pub/sub
  • Graph (Neo4j): Fraud detection, social networks, recommendation graphs

The ACID properties:

  • Atomicity: All-or-nothing transactions
  • Consistency: DB transitions between valid states only
  • Isolation: Concurrent transactions don't see each other's partial state
  • Durability: Committed transactions survive crashes (WAL)

23. Design a leaderboard system

Requirements: Real-time rankings, top-K queries, user rank lookup, score updates.

Redis Sorted Sets — the perfect data structure for this:

python
import redis

class Leaderboard:
    def __init__(self, redis_client, name: str):
        self.redis = redis_client
        self.key = f"leaderboard:{name}"

    def add_score(self, user_id: int, score: float):
        self.redis.zadd(self.key, {str(user_id): score})

    def increment_score(self, user_id: int, delta: float) -> float:
        return self.redis.zincrby(self.key, delta, str(user_id))

    def get_rank(self, user_id: int) -> int:
        # ZREVRANK: rank from highest (rank 0 = #1)
        rank = self.redis.zrevrank(self.key, str(user_id))
        return rank + 1 if rank is not None else None

    def get_top_k(self, k: int = 10) -> list[dict]:
        results = self.redis.zrevrange(self.key, 0, k - 1, withscores=True)
        return [
            {"rank": i + 1, "user_id": int(uid), "score": score}
            for i, (uid, score) in enumerate(results)
        ]

    def get_user_context(self, user_id: int, window: int = 5) -> list[dict]:
        # Return user's neighbors on the leaderboard
        rank = self.redis.zrevrank(self.key, str(user_id))
        if rank is None:
            return []
        start = max(0, rank - window)
        end = rank + window
        results = self.redis.zrevrange(self.key, start, end, withscores=True)
        return [
            {"rank": start + i + 1, "user_id": int(uid), "score": score}
            for i, (uid, score) in enumerate(results)
        ]

# O(log N) for add/update/rank-lookup
# O(log N + K) for top-K
lb = Leaderboard(redis_client, "weekly_game_2024_01")
lb.add_score(user_id=42, score=9850)
lb.get_top_k(10)

Persistence: Redis sorted sets are in-memory. Persist to PostgreSQL daily snapshots. For multi-region: replicate Redis or use a CRDT-based counter service.

Sharding large leaderboards: If a single leaderboard exceeds Redis memory (e.g., 100M players), partition into shards of 1M users each. Merge top-K from each shard to compute global top-K.


24. Design a type-ahead search (Google-style)

Already covered in Q14. Supplemental: how does Google scale this to billions of queries?

Multi-level architecture:

  1. 1Client-side: 300ms debounce, local LRU cache of recent prefix→results
  2. 2Edge cache (CDN): cache popular prefixes (top 1M prefixes cover ~80% of traffic)
  3. 3Regional cache (Redis): prefix → top-10 results, 5-minute TTL
  4. 4Backend trie service: falls through to this only on cold prefixes

Result personalization: Blend global popularity scores with user's search history. At query time:

final_score = 0.7 × global_freq + 0.3 × personal_freq

Spell correction: For unrecognized prefixes, run edit-distance-1 candidates through the trie. Limit candidates with BK-tree pruning.


25. Explain the saga pattern for distributed transactions

When a transaction spans multiple services, you can't use a single database ACID transaction. The saga pattern breaks it into a sequence of local transactions, each with a compensating transaction for rollback.

Choreography-based saga (event-driven):

OrderService         → publishes ORDER_CREATED
  InventoryService   → reserves stock → publishes STOCK_RESERVED
    PaymentService   → charges card → publishes PAYMENT_PROCESSED
      ShipmentService → creates shipment → publishes ORDER_FULFILLED

On failure at any step → publish failure event → compensating transactions run in reverse
  ShipmentService fails → publishes SHIPMENT_FAILED
    PaymentService (compensate) → refunds charge
      InventoryService (compensate) → releases reservation

Orchestration-based saga (central coordinator):

python
class OrderSaga:
    def __init__(self, order_id: int):
        self.order_id = order_id
        self.steps = [
            Step(
                action=lambda: inventory_service.reserve(order_id),
                compensation=lambda: inventory_service.release(order_id),
            ),
            Step(
                action=lambda: payment_service.charge(order_id),
                compensation=lambda: payment_service.refund(order_id),
            ),
            Step(
                action=lambda: shipment_service.create(order_id),
                compensation=lambda: shipment_service.cancel(order_id),
            ),
        ]

    def execute(self):
        completed = []
        for step in self.steps:
            try:
                step.action()
                completed.append(step)
            except Exception as e:
                # Rollback in reverse order
                for done_step in reversed(completed):
                    done_step.compensation()
                raise SagaRollbackError(f"Saga failed at step: {e}")

Trade-offs:

  • Sagas are ACD (not I — no isolation). Dirty reads between steps are possible.
  • Compensating transactions can't always perfectly undo (e.g., email sent → can't unsend)
  • Choreography is simpler but harder to trace; orchestration is observable but adds coupling

26. Design a real-time chat system (like Slack)

Requirements: 1:1 and group messages, real-time delivery, message history, read receipts, typing indicators.

Real-time delivery: WebSocket connection from each client to a gateway server.

python
# FastAPI WebSocket gateway (simplified)
from fastapi import WebSocket
import asyncio

connected_users: dict[int, WebSocket] = {}

async def websocket_endpoint(websocket: WebSocket, user_id: int):
    await websocket.accept()
    connected_users[user_id] = websocket

    try:
        while True:
            data = await websocket.receive_json()

            if data["type"] == "message":
                await handle_message(user_id, data)
            elif data["type"] == "typing":
                await broadcast_typing(user_id, data["channel_id"])
    finally:
        del connected_users[user_id]

async def handle_message(sender_id: int, data: dict):
    msg = await message_store.save({
        "channel_id": data["channel_id"],
        "sender_id": sender_id,
        "content": data["content"],
        "created_at": datetime.utcnow().isoformat(),
    })

    # Deliver to all online members of the channel
    members = await channel_service.get_members(data["channel_id"])
    for member_id in members:
        if member_id in connected_users:
            await connected_users[member_id].send_json({
                "type": "message",
                "message": msg,
            })
        else:
            # User is offline — send push notification
            await push_service.notify(member_id, msg)

Scaling WebSockets: Each gateway server handles ~10K–100K connections. With 10M concurrent users, that's 100–1,000 servers. Route user connections consistently (same user, same server) using consistent hashing on user_id at the load balancer.

Cross-server delivery: When user A (on server 1) sends a message to user B (on server 2), server 1 publishes to Redis pub/sub. Server 2 subscribes and delivers to B's WebSocket.

Message storage: Cassandra is ideal. Partition key: channel_id, clustering key: message_id (Snowflake, naturally ordered by time). Supports fast retrieval of most recent messages per channel.

sql
-- Cassandra
CREATE TABLE messages (
    channel_id  UUID,
    message_id  BIGINT,  -- Snowflake ID (time-ordered)
    sender_id   UUID,
    content     TEXT,
    PRIMARY KEY (channel_id, message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);

Offline message delivery: On reconnect, client sends last seen message_id. Server returns all messages with message_id > last_seen for each channel.


27. Design an API gateway

Responsibilities: Authentication, rate limiting, routing, SSL termination, request/response transformation, logging, circuit breaking.

python
class APIGateway:
    def __init__(self):
        self.auth = JWTAuthMiddleware()
        self.rate_limiter = TokenBucketRateLimiter(redis_client, capacity=100, refill_rate=10)
        self.router = ServiceRouter()
        self.circuit_breakers = {}  # service_name → CircuitBreaker

    async def handle(self, request: Request) -> Response:
        # 1. Auth
        try:
            user = await self.auth.authenticate(request)
        except AuthError:
            return Response(status=401, body="Unauthorized")

        # 2. Rate limiting
        if not self.rate_limiter.allow(user.id):
            return Response(status=429, body="Too Many Requests",
                          headers={"Retry-After": "1"})

        # 3. Route to upstream service
        service, path = self.router.resolve(request.path)

        # 4. Circuit breaker
        cb = self.circuit_breakers.setdefault(service, CircuitBreaker())

        try:
            upstream_response = await cb.call(
                http_client.forward, request, service, path
            )
        except Exception:
            return Response(status=503, body="Service temporarily unavailable")

        # 5. Response transformation (e.g., strip internal headers)
        return self.transform_response(upstream_response)

Service discovery: Gateway queries service registry (Consul, etcd, or Kubernetes endpoints) to find upstream service instances. Health check endpoints drive traffic away from unhealthy instances automatically.


28. Design a monitoring and alerting system

Components:

  • Metrics collection: Agents on each host (Prometheus exporters, StatsD) expose /metrics endpoints
  • Time series DB: Prometheus, InfluxDB, or Victoria Metrics
  • Query engine: PromQL for Prometheus
  • Alerting: AlertManager evaluates alert rules, routes to PagerDuty/Slack
  • Dashboards: Grafana

Key metric types:

  • Counter: monotonically increasing (requests_total, errors_total)
  • Gauge: current value (memory_usage_bytes, active_connections)
  • Histogram: distribution of values (request_duration_seconds)
  • Summary: similar to histogram but quantiles computed client-side
python
# Prometheus client instrumentation
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time

requests_total = Counter("http_requests_total", "Total HTTP requests", ["method", "endpoint", "status"])
request_duration = Histogram("http_request_duration_seconds", "Request duration", ["endpoint"])
active_connections = Gauge("active_connections", "Active WebSocket connections")

def track_request(func):
    def wrapper(request):
        start = time.time()
        try:
            response = func(request)
            requests_total.labels(
                method=request.method,
                endpoint=request.path,
                status=response.status_code
            ).inc()
            return response
        finally:
            request_duration.labels(endpoint=request.path).observe(time.time() - start)
    return wrapper

SLO/SLA design:

  • SLI (indicator): actual measured metric (e.g., p99 latency = 120ms)
  • SLO (objective): target (e.g., p99 latency < 200ms, 99.9% of requests)
  • Error budget: 100% - SLO. If 99.9% SLO, you have 0.1% budget = 43.8 min/month downtime allowed
  • Burn rate alerts: alert when error budget is being consumed too fast

29. How do you handle database migrations in production?

Golden rule: Migrations must be backwards-compatible. The old version of the app must work with the new schema, and vice versa. This enables zero-downtime deploys.

Expand-Contract pattern (for zero-downtime column rename):

sql
-- Step 1: Expand — add new column (both versions work)
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

-- Step 2: Migrate data (run as a background job)
UPDATE users SET full_name = first_name || ' ' || last_name WHERE full_name IS NULL;

-- Step 3: Deploy new code that reads full_name (old code still reads first_name)

-- Step 4: Contract — drop old columns (only after old code is fully retired)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;

For adding an index without locking:

sql
-- Standard: locks the table
CREATE INDEX ON users (email);

-- PostgreSQL: build index without locking (takes longer but safe in prod)
CREATE INDEX CONCURRENTLY ON users (email);

For large table migrations: Never run ALTER TABLE on a table with 500M rows during business hours. Use a shadow table approach:

  1. 1Create new table with new schema
  2. 2Copy rows in batches of 10K (with sleep between batches)
  3. 3Trigger/changelog to capture changes during migration
  4. 4Atomic rename: RENAME TABLE users TO users_old, users_new TO users

Tools: gh-ost (GitHub) for MySQL, pg_repack for PostgreSQL.


30. Design an e-commerce inventory system

The core problem: Prevent overselling (selling more units than you have in stock) under high concurrent load (flash sales, Black Friday).

Naive approach fails:

python
# WRONG: race condition
def purchase(product_id, quantity):
    stock = db.query("SELECT stock FROM inventory WHERE id = ?", product_id)
    if stock >= quantity:  # another request reads stock=5 here too
        db.execute("UPDATE inventory SET stock = stock - ? WHERE id = ?", quantity, product_id)
        # Both succeed → stock goes negative

Correct approach — optimistic locking:

sql
UPDATE inventory
SET stock = stock - :quantity, version = version + 1
WHERE product_id = :product_id
  AND stock >= :quantity
  AND version = :expected_version;
-- Returns 0 rows updated → retry with fresh data

Or: Redis atomic decrement with floor:

python
def reserve_stock(product_id: int, quantity: int) -> bool:
    key = f"stock:{product_id}"
    # Lua script: atomic check-and-decrement
    script = """
    local stock = tonumber(redis.call("get", KEYS[1]))
    if stock == nil or stock < tonumber(ARGV[1]) then
        return 0
    end
    redis.call("decrby", KEYS[1], ARGV[1])
    return 1
    """
    result = redis.eval(script, 1, key, quantity)
    if result:
        # Async: persist reservation to DB
        queue.enqueue("persist_reservation", product_id, quantity)
    return bool(result)

Flash sale architecture:

  • Pre-load stock counts into Redis before sale starts
  • All stock checks go through Redis (not DB)
  • DB updated asynchronously via queue
  • Use Lua scripts for atomic operations (no TOCTOU race)
  • Queue overflow: return "queue position" token if stock exhausted momentarily — let user wait

31. How do you design a CDN?

Components:

  • Edge nodes (Points of Presence, PoPs) distributed globally
  • Origin servers (your actual infrastructure)
  • DNS-based traffic routing (Anycast or GeoDNS → nearest edge)

Caching hierarchy:

Browser cache (TTL: seconds–minutes)
  → CDN edge cache (TTL: minutes–hours)
    → CDN origin shield (aggregates misses, reduces origin load)
      → Your origin servers

Cache-Control headers:

Cache-Control: public, max-age=86400          # cache for 1 day
Cache-Control: public, max-age=31536000, immutable  # forever (hashed filenames)
Cache-Control: no-store                       # never cache (sensitive data)
Vary: Accept-Encoding                         # separate cache for gzip vs brotli

Cache invalidation: The hardest problem. Two strategies:

  1. 1Content-addressed URLs: bundle.a3f9c1.js — hash in filename means new content = new URL = no invalidation needed (set immutable, max-age forever)
  2. 2Purge API: explicitly invalidate specific URLs or tag-based groups (cdn.purge(tag="product:123"))

For API responses (dynamic CDN):

  • Short TTL (5–60 seconds) handles most traffic for popular content
  • Surrogate-Key / Cache-Tag headers enable targeted purges
  • stale-while-revalidate serves stale content while fetching fresh in background

32. Explain eventual consistency with a concrete example

Eventual consistency means: if no new updates are made to a piece of data, eventually all reads will return the same (latest) value. There is no guarantee of when this happens.

Concrete example: shopping cart in a distributed system

User adds item on mobile (writes to US-West datacenter). User checks cart on laptop 200ms later, request routes to US-East datacenter. Replication lag = 150ms. Cart appears empty for 150ms, then the item appears. This is eventual consistency.

Conflict resolution strategies:

Last-write-wins (LWW): timestamp wins. Simple but can lose data if clocks are skewed.

CRDTs (Conflict-free Replicated Data Types): data structures that merge without conflict:

python
# G-Counter (grow-only counter): each node increments its own slot
class GCounter:
    def __init__(self, node_id: str, nodes: list[str]):
        self.node_id = node_id
        self.counts = {n: 0 for n in nodes}

    def increment(self, delta=1):
        self.counts[self.node_id] += delta

    def value(self) -> int:
        return sum(self.counts.values())

    def merge(self, other: 'GCounter'):
        # Take element-wise maximum — always safe, no conflicts
        for node, count in other.counts.items():
            self.counts[node] = max(self.counts[node], count)

Shopping cart CRDT: add-wins set (OR-Set) — add and remove are both tagged with unique IDs; concurrent add+remove resolves to "added".


33. Design an ad serving system

Requirements: Serve relevant ads to users. < 50ms latency. Billions of ad auctions per day.

Flow:

User visits page → Publisher sends bid request (user signals, page context)
                → Ad Exchange (real-time auction)
                → DSPs (Demand-Side Platforms) respond with bids in < 100ms
                → Highest bidder wins, ad served
                → Impression/click events logged

Targeting: Segment users by demographics, interests, behavioral signals (pages visited, purchases). Store in fast key-value store (Redis, Aerospike).

python
def run_auction(bid_request: BidRequest) -> AdCreative:
    # 1. Retrieve eligible ads for this user+context (< 5ms)
    user_segments = segment_store.get(bid_request.user_id)
    eligible_ads = ad_index.query(
        segments=user_segments,
        page_category=bid_request.page_category,
        format=bid_request.format,
    )

    # 2. Score each ad (relevance × bid price)
    scored_ads = [
        (ad, predict_ctr(ad, bid_request) * ad.max_cpm)
        for ad in eligible_ads
    ]

    # 3. Second-price auction (Vickrey): winner pays second-highest bid + $0.01
    scored_ads.sort(key=lambda x: x[1], reverse=True)
    winner, winning_score = scored_ads[0]
    clearing_price = scored_ads[1][1] + 0.01 if len(scored_ads) > 1 else scored_ads[0][1]

    # 4. Log impression asynchronously
    event_bus.publish("impression", {
        "ad_id": winner.id, "user_id": bid_request.user_id,
        "price": clearing_price, "timestamp": time.time()
    })

    return winner.creative

CTR prediction: Logistic regression or gradient boosted trees on features (user segments, ad creative, hour of day, device type). Served from model cache (< 1ms inference).

Frequency capping: Track per-user ad exposure in Redis. INCR user:{uid}:ad:{ad_id}:day with 24h TTL.


34. Design a logging and analytics pipeline

Requirements: Ingest billions of events/day, real-time dashboards, historical queries, low-cost storage.

Lambda architecture:

  • Speed layer: Kafka → Flink/Spark Streaming → Redis/Druid (real-time metrics, ~1 min latency)
  • Batch layer: Kafka → S3 → Spark (historical processing, accurate, higher latency)
  • Serving layer: Druid + ClickHouse for OLAP queries across both layers

Modern Kappa architecture (simpler, Kafka-native):

  • Everything is a stream. Reprocess historical data by replaying Kafka from offset 0.
  • Kafka retention: 7–30 days for hot data, archive to S3 for cold.

ClickHouse for analytics (extremely fast OLAP):

sql
-- ClickHouse table for events
CREATE TABLE events (
    event_time   DateTime,
    user_id      UInt64,
    event_type   LowCardinality(String),
    session_id   String,
    properties   Map(String, String)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, user_id, event_time);

-- Query: DAU for last 30 days
SELECT toDate(event_time) AS date, uniq(user_id) AS dau
FROM events
WHERE event_type = 'session_start'
  AND event_time >= now() - INTERVAL 30 DAY
GROUP BY date ORDER BY date;
-- Runs in < 1 second on billions of rows

Event schema design (use a consistent schema across all events):

json
{
  "schema_version": "1.0",
  "event_id": "uuid",
  "event_type": "page_view",
  "timestamp": "2024-01-15T10:30:00Z",
  "user_id": 12345,
  "session_id": "abc123",
  "properties": {
    "page": "/product/123",
    "referrer": "google.com",
    "device": "mobile"
  }
}

35. How do you design for data privacy and GDPR compliance?

Right to be forgotten:

python
def delete_user_data(user_id: int):
    # Hard delete PII from all primary stores
    db.execute("DELETE FROM users WHERE id = ?", user_id)
    db.execute("DELETE FROM user_profiles WHERE user_id = ?", user_id)

    # Anonymize historical events (keep for analytics, remove PII)
    db.execute(
        "UPDATE events SET user_id = NULL, ip_address = NULL WHERE user_id = ?",
        user_id
    )

    # Purge from caches
    redis.delete(f"user:{user_id}:*")  # scan and delete all user keys

    # Purge from search indexes
    elasticsearch.delete_by_query({"term": {"user_id": user_id}})

    # Queue purge from backups (harder — flag for exclusion in backup restore)
    deletion_log.record(user_id, requested_at=datetime.utcnow())

The backup problem: You can't easily delete from backups. Solution: encrypt user data with a per-user encryption key. To "delete" the user, delete their key. Their data in backups becomes unreadable.

Data minimization: Only collect what you need. Use field-level encryption for sensitive data (SSN, full credit card number).

Consent management: Store explicit consent records with timestamp and version of privacy policy. Gate data processing on consent scope.


36. Design a job scheduling system (like cron at scale)

Requirements: Schedule jobs to run at specific times or intervals. Distributed, fault-tolerant, exactly-once execution.

python
# Job definition
{
    "job_id": "send-weekly-digest",
    "cron_expression": "0 9 * * MON",  # every Monday at 9am
    "timezone": "America/New_York",
    "handler": "digest_service.send_weekly",
    "timeout_sec": 300,
    "max_retries": 3,
    "concurrency_policy": "forbid"  # don't run if previous still running
}

Core algorithm:

  1. 1Leader election (Zookeeper/etcd) — only one scheduler node triggers jobs
  2. 2Scan next-run-times every 10 seconds, enqueue due jobs to Kafka/SQS
  3. 3Workers consume from queue, execute job, report result
  4. 4Distributed locking per job prevents duplicate execution (even with multiple workers)
python
def check_and_schedule_jobs():
    now = datetime.utcnow()
    due_jobs = db.query(
        "SELECT * FROM jobs WHERE next_run_at <= ? AND status = 'active'",
        now
    )
    for job in due_jobs:
        # Atomic claim: only one scheduler picks up this job
        updated = db.execute(
            "UPDATE jobs SET status = 'scheduled', next_run_at = ? "
            "WHERE job_id = ? AND status = 'active' AND next_run_at <= ?",
            compute_next_run(job.cron_expression), job.job_id, now
        )
        if updated.rowcount == 1:
            queue.enqueue("job_executions", {"job_id": job.job_id, "trigger_time": now})

Idempotent execution: Pass trigger_time as idempotency key. If a job runs twice for the same trigger time, the second run is a no-op.


37. Design a search engine (like Elasticsearch)

Inverted index — the core data structure:

Document 1: "the quick brown fox"
Document 2: "the lazy brown dog"

Inverted index:
  "the"   → [1, 2]
  "quick" → [1]
  "brown" → [1, 2]
  "fox"   → [1]
  "lazy"  → [2]
  "dog"   → [2]

Query "brown fox" → intersection of [1,2] and [1] = [1]
python
from collections import defaultdict
import re

class InvertedIndex:
    def __init__(self):
        self.index: dict[str, set[int]] = defaultdict(set)
        self.documents: dict[int, str] = {}

    def tokenize(self, text: str) -> list[str]:
        # Lowercase, split on non-alpha, remove stopwords
        stopwords = {"the", "a", "an", "is", "are", "was"}
        tokens = re.findall(r'[a-z]+', text.lower())
        return [t for t in tokens if t not in stopwords]

    def add_document(self, doc_id: int, content: str):
        self.documents[doc_id] = content
        for token in set(self.tokenize(content)):
            self.index[token].add(doc_id)

    def search(self, query: str) -> list[int]:
        tokens = self.tokenize(query)
        if not tokens:
            return []
        result = self.index.get(tokens[0], set())
        for token in tokens[1:]:
            result = result & self.index.get(token, set())
        return sorted(result)

TF-IDF scoring (term frequency × inverse document frequency):

  • TF: how often term appears in document (normalized by doc length)
  • IDF: log(total_docs / docs_containing_term) — rewards rare terms
  • Higher score = more relevant

Sharding: Documents distributed across shards by doc_id hash. Queries fan out to all shards, results merged and re-ranked. Elasticsearch calls shards "primary shards."

Real-time indexing: Write to Kafka → indexer workers update index segments → Lucene segments merged in background (segment compaction, similar to LSM tree).


38. Design a feature flag system

Requirements: Enable/disable features per user, cohort, or percentage. Instant rollout/rollback. No code deploys needed.

python
class FeatureFlagService:
    def __init__(self, redis_client, db):
        self.redis = redis_client
        self.db = db
        self.local_cache = {}  # in-process cache, TTL 30s

    def is_enabled(self, flag_name: str, user_id: int, context: dict = None) -> bool:
        flag = self._get_flag(flag_name)
        if not flag:
            return False  # unknown flags default to off

        # Killswitch: globally disabled
        if not flag["enabled"]:
            return False

        # User-specific override
        if user_id in flag.get("user_overrides", {}):
            return flag["user_overrides"][user_id]

        # Cohort-based (e.g., beta users, employees)
        if context and context.get("cohort") in flag.get("allowed_cohorts", []):
            return True

        # Percentage rollout (deterministic: same user always gets same result)
        if "rollout_percentage" in flag:
            bucket = hash(f"{flag_name}:{user_id}") % 100
            return bucket < flag["rollout_percentage"]

        return flag.get("default", False)

    def _get_flag(self, name: str) -> dict:
        # L1: in-process cache
        if name in self.local_cache and not self._is_stale(name):
            return self.local_cache[name]

        # L2: Redis
        cached = self.redis.get(f"flag:{name}")
        if cached:
            flag = json.loads(cached)
            self.local_cache[name] = flag
            return flag

        # L3: DB
        flag = self.db.get_flag(name)
        if flag:
            self.redis.setex(f"flag:{name}", 30, json.dumps(flag))
            self.local_cache[name] = flag
        return flag

Push invalidation: When a flag is updated, push invalidation event via Redis pub/sub to clear all in-process caches immediately. This ensures rollback is instant, not limited to the 30-second TTL.


39. What are the most common system design mistakes?

1. Over-engineering from the start

Starting with microservices, Kafka, and 3 data stores before you have 100 users. Start monolithic, extract services when you have specific, measured scaling problems.

2. Not clarifying requirements

Spending 45 minutes designing a globally distributed system when the interviewer only needed a single-region service at 10K RPS.

3. Ignoring the data model

Components and architecture without a concrete schema. The schema drives every query, every index, every scaling decision.

4. Forgetting failure modes

What happens when the cache is cold? When the DB primary fails? When a Kafka consumer lags? Every system must have an answer.

5. Premature optimization of the wrong bottleneck

Adding a cache before profiling to confirm the DB is actually the bottleneck. Adding read replicas when the issue is N+1 queries.

6. Ignoring operational concerns

How do you deploy it? How do you monitor it? How do you debug it at 3am? A system that can't be operated is not a real system.

7. Designing for average case, not tail latency

Average latency 20ms, p99 = 5 seconds. The p99 is the experience of 1% of users — at 10M RPS that's 100K users per second having a bad time.


40. How do you talk about trade-offs in a system design interview?

This is the most important meta-skill. Interviewers at FAANG are explicitly evaluating whether you can hold two conflicting ideas simultaneously and reason about which is better given specific constraints.

Framework for discussing any trade-off:

  1. 1Name both sides explicitly: "We can do X or Y"
  2. 2State what each optimizes for: "X gives us lower latency but higher write amplification; Y gives us simpler operations but slower reads"
  3. 3Tie to requirements: "Given that this is a read-heavy system with 100:1 read/write ratio, X is the better fit"
  4. 4Acknowledge the cost: "We accept the write amplification because..."

Common trade-off pairs to know cold:

| Trade-off | Choose A when... | Choose B when... |

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

| SQL vs NoSQL | Complex queries, transactions | Write scale, flexible schema |

| Strong vs eventual consistency | Financial data, inventory | Social feeds, product catalogs |

| Fan-out on write vs read | Read-heavy, many followers | Write-heavy, celebrities (many followers) |

| Normalization vs denormalization | Write-heavy, storage cost | Read-heavy, query performance |

| Monolith vs microservices | Small team, early stage | Large team, independent scaling needs |

| Push vs pull (notifications) | Fewer receivers, real-time | Many receivers, polling acceptable |

| Cache-aside vs write-through | Tolerates stale data | Needs fresh data on every read |

| Synchronous vs async processing | User needs immediate result | User tolerates eventual result |

The answer interviewers want: Not "X is better than Y" but "X and Y are better in different contexts, and here are the specific signals I'd look for to decide."


Quick Reference: Numbers Every Engineer Must Know

Latency                          Throughput
─────────────────────────────    ─────────────────────────────────
L1 cache:         0.5 ns         Single-core compute:  ~1B ops/sec
L2 cache:           5 ns         Redis:              ~1M ops/sec
RAM:               100 ns        Postgres writes:    ~5K-10K/sec
SSD random read:   100 µs        Kafka:              ~1M msgs/sec
Network (same DC): 0.5 ms        S3 GET:             ~5,500 req/s
Network (cross):   100 ms        CDN edge:          ~100K req/s

Storage
─────────────────────────────────────────
1B users × 1KB profile  = 1 TB
1B rows × 100 bytes     = 100 GB
1M req/sec × 1KB/req    = 1 GB/sec ingress
100 bytes/msg × 1M msg/s = 100 MB/s Kafka throughput

Final Advice

System design interviews reward candidates who can structure ambiguity. The engineer who asks three sharp clarifying questions before drawing a single box will consistently outperform the one who launches immediately into a fully-detailed architecture.

Practice the framework until the structure is automatic. Then the cognitive load of "what to say next" drops away, and you can focus on the actual trade-offs — which is where the real signal lives.

One system to practice that covers nearly every component: design Uber. You will touch real-time location updates (WebSocket, geospatial indexing), matching algorithm (geohash, priority queues), payment processing (idempotency, saga), surge pricing (time-series aggregation), and notifications (push, SMS). Design it once completely, then design it again a week later without looking at your notes. If you can do it cleanly in 45 minutes, you are ready.

FAQ

How long should I spend on requirements clarification in a system design interview?+

Spend 5 minutes maximum. Ask about scale (DAU, RPS), consistency requirements (can users see stale data?), read/write ratio, latency SLA, and geographic distribution. State your assumptions out loud if the interviewer doesn't answer. The goal is to demonstrate that you don't design in a vacuum — every architectural decision flows from these constraints. Spending more than 5 minutes looks like stalling.

Should I always use microservices in a system design interview?+

No. Microservices are a scaling solution for team and deployment independence, not a default architecture. For most interview prompts, start by describing how you would build it as a modular monolith, then explain which components you would extract as separate services and why (e.g., 'the transcoding pipeline has very different scaling characteristics from the metadata API, so I'd run those independently'). Proposing microservices without justifying the trade-offs signals pattern matching, not engineering judgment.

When should I use Kafka versus a simple database queue?+

Use Kafka when: (1) you need multiple independent consumer groups reading the same stream, (2) you need to replay events (audit, reprocessing), (3) throughput exceeds 10K messages/sec, (4) you want strict ordering within a partition. Use a database-backed queue (e.g., PostgreSQL SKIP LOCKED, SQS) when: simpler operational model is more important, you have moderate throughput, or you need exactly-once semantics without Kafka transactions complexity. Kafka's operational overhead is real — don't reach for it reflexively.

What's the single most important thing to know about caching?+

Cache invalidation is harder than it looks. The three failure modes are: serving stale data after the source changes (cache not invalidated), cache stampede (many requests miss simultaneously after expiry), and hot key problem (one key receives disproportionate traffic, overwhelms one cache node). For invalidation: prefer TTL + short-lived caches for simplicity, use write-through when freshness is critical, and use content-addressed keys (URL includes hash of content) when you can change the key rather than invalidate it. For stampede: use probabilistic early expiration or a mutex on the first miss. For hot keys: use local in-process L1 caches or key replication.

How do I explain database sharding in an interview without getting lost in details?+

Lead with the problem: 'A single PostgreSQL instance tops out at roughly 10K writes/sec and a few TB of storage. At our projected scale we need more.' Then state the strategy (hash sharding by user_id is the most common choice), explain the implication for queries (cross-shard queries require scatter-gather in the application layer), explain how you'd handle distributed IDs (Snowflake IDs instead of auto-increment), and name the main operational challenge (resharding when you need to add nodes, mitigated by consistent hashing). That's a complete sharding discussion in under 3 minutes.

What is the CAP theorem and how should I use it in an interview?+

CAP theorem says a distributed system can guarantee at most two of: Consistency (every read returns the latest write), Availability (every request gets a response), and Partition Tolerance (system works despite network splits). Since network partitions always happen, the real choice is CP versus AP. Use it in interviews by naming your choice explicitly: 'This is a financial ledger, so I'll choose CP — I'd rather return an error than serve stale balance data.' Or: 'This is a social feed, so AP is fine — users tolerate slightly stale content.' The interviewer wants to hear you make a conscious choice and connect it to the product requirements, not just recite the theorem.

How do I handle the 'how would you scale this to 100x' follow-up question?+

Walk through the bottlenecks systematically: (1) Read bottleneck → add read replicas, add cache layer, move to CDN for static content; (2) Write bottleneck → shard the database by the primary access key, use async writes where possible, switch from row-level locking to optimistic concurrency; (3) Compute bottleneck → horizontal auto-scaling, offload expensive work to async workers; (4) Network bottleneck → compress payloads, add edge caching, reduce payload size via pagination or field selection; (5) Storage bottleneck → archive cold data to cheaper storage (S3 Glacier), partition tables by time, move blobs out of the DB into object storage. Always state which bottleneck you hit first given the specific read/write ratio of the system.

Should I memorize specific numbers for back-of-envelope calculations?+

Yes, a core set. The ones that come up in almost every interview: 1 million DAU generates ~10-100 million requests/day depending on product; 1 byte = 8 bits, 1 KB ≈ 1,000 bytes, 1 TB = 10^12 bytes; SSD random read latency ~100 µs, network round-trip same data center ~0.5 ms, cross-region ~100 ms; RAM is ~1,000x faster than SSD; a single PostgreSQL node handles ~5-10K writes/sec, ~50K reads/sec; Redis handles ~100K-1M ops/sec; Kafka handles millions of messages/sec per broker. You don't need precision — order-of-magnitude accuracy is enough. The calculation demonstrates that you understand scale implications, not that you can compute exact numbers.

Artículos relacionados

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+.

Preparate para tu entrevista real

Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.

Empezar gratis →

¿Tenés entrevista próxima? Instalá el copiloto en vivo →

InterviewHack.ai

Preparate para la entrevista exacta: quién te entrevista, tu CV a medida y coach real.

Producto

VacantesRevisar CV (ATS) gratis¿Cómo suena tu inglés?¿Te pagan bien?Reporte de sueldos LATAMCursos gratisBlogCV a medidaPráctica habladaEs gratis

Empleos remotos

ReactPythonFull-StackLATAMArgentinaMéxicoVer todas →

Preparate

Práctica habladaFrontendBackendAI EngineerPor empresaVendete con tu CV

Empresa

Buscás talentoAcerca deContactoPrivacidadTérminos

© 2026 InterviewHack.ai · Tu CV es tuyo. Nunca se usa para entrenar nada. · Un producto de IA-PTY