Skip to content

Algorithms vs. System Design

Two kinds of problem, two distinct skills β€” but the same structured way of reasoning.

Algorithm Questions

Closed problem

They usually have a deterministic solution. The focus is on the correctness of the algorithm, its O(n) complexity and a clean implementation. Examples: "find the shortest path", "check whether a tree is a BST".

System Design Questions

Open problem

There is no single answer. The focus is on tradeoffs, scalability, communication and engineering reasoning. Examples: "design Twitter", "design a URL shortening system for 100M users".

"Tackle the problem by doing it just like you would at work."
Ask questions. Engage the interviewer. Discuss the tradeoffs. There is no perfect answer β€” there is well-articulated reasoning and justified solutions.

Essential mindset

In system design, the interviewer wants to see how you think, not just what you have memorized. A candidate who asks good questions, makes assumptions explicit and admits uncertainty with solid reasoning often outperforms a candidate who tries to recite a ready-made architecture.

What the interviewer evaluates

Clear communication Structured reasoning Technical knowledge Managing tradeoffs Realistic estimates Spotting bottlenecks
Aspect Algorithm Interview System Design Interview
Correct answerDeterministic, usually uniqueMultiple valid solutions
FocusCorrectness and complexityTradeoffs and scalability
Typical length20–45 min45–60 min
CommunicationImportantCritical β€” it is the main evaluation
Whiteboard/diagramUsefulMandatory

Handling the Questions

8 principles to run a system design interview with confidence and clarity.

1
Communicate Think out loud. The interviewer wants to follow your reasoning, not just see the final solution. Long silence is the biggest enemy in a system design interview β€” even if you don't know the answer yet, say what you are thinking: "I'm considering a cache here because..."
2
Go broad first Before diving into details, sketch the whole system at a high level. Establish the main components before going deep into any of them. Detailing a component too early, only to redesign it later, is a common mistake.
3
Use the whiteboard Draw! Diagrams help both the interviewer and you keep the reasoning organized. A clear block diagram communicates more than minutes of verbal explanation. Start simple and refine.
4
Acknowledge interviewer concerns If the interviewer raises an objection or points out a problem, don't ignore it or defend your position at all costs. Say: "good point, let me reconsider that part". This shows technical maturity and openness to feedback.
5
Be careful about assumptions Implicit assumptions are traps. If you assume the system has 1,000 users when it actually has 1 billion, your whole solution may be wrong. Ask before you assume.
6
State your assumptions explicitly When you need to assume something (which is unavoidable), say it out loud: "I'll assume the system must support 10M daily active users, peaking at 100k req/s". The interviewer can correct it, confirm it or give more context.
7
Estimate when necessary Back-of-envelope calculations are expected and valued. Know how to make quick estimates of throughput, storage and bandwidth. E.g.: "1M users Γ— 100KB/user = ~100GB of storage". It doesn't need to be exact β€” the order of magnitude is what matters.
8
Drive You lead the session. Don't wait for the interviewer to tell you what to do next. Propose the next step: "I'll focus on the database layer now, OK?" or "I see two main bottlenecks β€” let me tackle the most critical one first". Take ownership.

Practical tip: Use explicit transitions between stages. When moving from "main components" to "identifying bottlenecks", say: "Now that we have the main components, I'll identify the critical points of this system." This keeps the interviewer oriented in your structure.


Design: Step-By-Step

A 5-step structure to design complex systems in an organized way during the interview.

1
Scope the Problem Ask questions to narrow down the problem. How many users? Which features are essential vs. nice-to-have? Is the system read-intensive or write-intensive? Is strong consistency required, or is eventual consistency enough? Define the scope before any architecture. Typical questions: "What are the main features I should design?", "What scale do we expect in 1 year?", "Are there latency constraints?"
2
Make Reasonable Assumptions With the scope defined, state explicit assumptions: "I'll assume 10M DAU, a peak of 100k req/s, 99.9% availability (at most ~8.7h of downtime per year), and that the system is far more read-heavy than write-heavy (100:1)". This anchors the rest of the conversation and shows that you think in concrete metrics.
3
Draw the Major Components Sketch the main blocks: clients β†’ load balancers β†’ web/app servers β†’ caches β†’ databases β†’ message queues β†’ CDN. Don't go into details yet β€” big picture first. Each block must have a clear purpose. Label the arrows with the kind of data flowing between components.
4
Identify the Key Issues Where are the bottlenecks? What are the single points of failure? Which components will "break" first as the system grows? Is the database the read bottleneck? Does the application server hold state that prevents horizontal scaling? Identify the critical points before proposing solutions.
5
Redesign for the Key Issues Now you go deeper: add sharding to the database, caching on hot paths, horizontal scaling of the app servers, a CDN for static assets, queues for asynchronous processing. Redesign iteratively, focusing on the problems you identified. Justify each decision with the tradeoffs involved.
example β€” scalable URL shortener architecture
CDN static Client browser/app Load Balancer App Server 1 App Server 2 App Server 3 Cache Redis miss DB Primary MySQL / PG DB Replica read-only repl. Queue Kafka/SQS Analytics workers Analytics DB step 1 scope steps 2–3 components step 4–5 bottlenecks / redesign async processing decoupling
Key scoping question

Always ask: "Is this system read-heavy or write-heavy?" β€” it changes everything.
Read-heavy (e.g. URL shortener, Twitter timeline) β†’ aggressive caching, DB read replicas.
Write-heavy (e.g. log ingestion, real-time chat) β†’ queues + asynchronous processing, eventual consistency.


Algorithms that Scale: Step-By-Step

When the problem is algorithmic but must run at massive scale β€” data that does not fit in one machine.

Unlike pure system design, here you start from a correct algorithm and iterate until it works with billions of records. The key is to recognize where the solution "breaks" as it scales and to address exactly those points.

1
Ask Questions How much data? What is the distribution? What are the time and memory constraints? Does the data fit in RAM? Are there duplicates? Is it sorted? Are approximations (approximate algorithms) acceptable, or is exact precision required? The right questions reveal the real problem and the applicable techniques.
2
Make Believe Assume the data fits in a single machine and solve the problem there. Find the correct algorithm, ignoring scale for now. This gives you the baseline solution you will adapt. E.g.: for the top-K words in 1TB of logs, you would start with a HashMap + a MinHeap of size K.
3
Get Real Now face reality: the data doesn't fit in RAM, or doesn't fit on one machine's disk, or serial processing would be too slow. How do you split the problem? Hash partitioning? Sampling? Streaming? Which parts can be parallelized?
4
Solve Problems Identify which parts of the step 2 solution break at scale and fix exactly those points. Sharding, MapReduce, approximate counting (HyperLogLog for distinct counts, Count-Min Sketch for frequencies), reservoir sampling for streaming data.
Full example

Top-K words in 1TB of logs

Make believe: HashMap<String, Integer> to count + a MinHeap of size K for the top-K. O(N log K).

Get real: 1TB doesn't fit in RAM (assume ~10GB of heap).

Solve: Split the file into N chunks. Each worker processes one chunk β†’ emits (word, count). A central reducer aggregates counts per word and keeps a global MinHeap of size K.

Alternative: MapReduce β€” Map emits (word, 1), Reduce sums, each reducer keeps a local top-K, final merge.

Scaling techniques

When to use each approach

MapReduce: batch, parallelizable data; fits word count, PageRank, joins of large tables.

HyperLogLog: counts distinct elements with ~1% error using O(log log N) memory. Ideal for "how many unique users visited?"

Count-Min Sketch: approximate frequency of elements. Ideal for top-K with high cardinality.

Bloom Filter: probabilistic membership check. "Has this URL already been visited?" with zero false negatives.


Back-of-the-Envelope

Reference numbers for quick estimates β€” the order of magnitude is what matters, not precision.

1 day
~10⁡ s
86,400 s. Round it to 100k and do the math in your head.
1M req/day
~12/s
That is the average. Typical peak: 2–5Γ— the average β€” size for the peak.
Read : Write
100:1
Typical ratio of read-heavy systems (feeds, URL shortener, catalogs).

Latencies every engineer should know

OperationApprox. latencyImplication
L1 cache reference~1 nsCPU is practically "free" compared to I/O
Main memory (RAM) access~100 nsThat's why in-memory caches (Redis) answer in sub-ms
Read 1 MB sequentially from RAM~5–10 ΞΌsScanning data in memory is cheap
Random SSD read (4 KB)~20–100 ΞΌs~1,000Γ— slower than RAM
Round trip within the same datacenter~0.5 msEvery call between microservices costs at least this much
Read 1 MB sequentially from SSD~0.2–1 msSequential reads are much cheaper than random ones
Spinning disk (HDD) seek~5–10 msAvoid random HDD access on the critical path
Round trip SΓ£o Paulo ↔ Virginia (us-east-1)~110–130 msAvoid synchronous cross-region calls; use CDN/edge

Availability β€” the "nines"

SLODowntime / yearDowntime / month
99%~3.65 days~7.3 h
99.9%~8.77 h~43.8 min
99.95%~4.38 h~21.9 min
99.99%~52.6 min~4.38 min
99.999%~5.26 min~26.3 s
Powers of 2

Storage units

2¹⁰ β‰ˆ 10Β³ β†’ KB (thousand)
2²⁰ β‰ˆ 10⁢ β†’ MB (million)
2³⁰ β‰ˆ 10⁹ β†’ GB (billion)
2⁴⁰ β‰ˆ 10ΒΉΒ² β†’ TB (trillion)
2⁡⁰ β‰ˆ 10¹⁡ β†’ PB

Typical sizes

To estimate storage

Numeric ID / timestamp: 8 B
UUID: 16 B (36 B as text)
"Small" record (URL, short post): ~0.5–1 KB
Compressed photo: ~200 KB – 2 MB
1 min of HD video: ~50–100 MB

How to present an estimate

Round aggressively, do the math out loud and, above all, say what the number changes in the architecture. "That's ~3 TB in 5 years β€” it fits in one database with replicas, so sharding is a future optimization, not a day-1 requirement." An estimate that does not lead to a decision is just arithmetic.


Key Concepts

The concepts that show up in every system design interview β€” click each item to expand the explanation.

Vertical scaling (scale up): adds more CPU, RAM or disk to the same machine. It is simple β€” no code changes required. But it has a physical and hardware limit (there is no machine with infinite RAM), creates a single point of failure, and cost grows exponentially with capacity.

Horizontal scaling (scale out): adds more machines. Theoretically unlimited and cheaper at scale. It requires stateless application servers, load balancing and coordination between nodes. Most large-scale web systems scale horizontally.

Practical strategy: start vertical (simpler, zero refactoring), but design for horizontal from the start. When vertical hits a cost or physical limit, move to horizontal. The mistake is not planning for this from the beginning.

AspectVerticalHorizontal
ImplementationSimpleComplex
LimitMaximum hardwarePractically unlimited
CostGrows exponentiallyGrows linearly
Single point of failureYesNo (with redundancy)
Downtime when expandingUsually yesNo

Distributes requests across multiple backend servers, preventing any server from being overloaded while others sit idle. It also runs health checks and automatically removes failed servers from the pool.

Distribution algorithms:

  • Round-robin: each server receives requests in turn. Simple, but ignores the current load.
  • Weighted round-robin: more powerful servers receive a larger share of requests.
  • Least connections: forwards to the server with the fewest active connections. Better for requests of variable duration.
  • IP hash: shard = hash(client_IP) % N. Guarantees sticky sessions (same client β†’ same server). Useful when the server holds state.

Implementation types: Hardware (F5, Cisco), software (Nginx, HAProxy, Envoy), cloud-managed (AWS ALB/NLB, GCP Cloud Load Balancing).

Watch out: the load balancer itself can become a single point of failure. Solution: active/passive pairs (primary + standby with automatic failover) or DNS-based load balancing (multiple IPs in DNS with a short TTL).

Denormalization: denormalizing a relational database means strategically duplicating data to avoid expensive JOINs. You trade consistency (updates must be made in several places) for read performance. It works well when reads far outnumber writes and JOINs are a proven bottleneck.

NoSQL β€” when and which to use:

  • Key-Value (Redis, DynamoDB): O(1) access by key. Ideal for sessions, caching, leaderboards, rate limiting.
  • Document (MongoDB, Couchbase): semi-structured data, flexible schema. Ideal for product catalogs, user profiles.
  • Column-family (Cassandra, HBase): massive distributed writes, queries by partition key. Ideal for time series, IoT, logs.
  • Graph (Neo4j, Amazon Neptune): complex relationships between entities. Ideal for social graphs, recommendation systems, fraud detection.

CAP Theorem: in a distributed system, you can guarantee at most 2 of the 3 properties: Consistency (all nodes see the same data), Availability (every request gets a response), Partition tolerance (the system keeps working despite network failures). In practice, P is unavoidable β€” you choose between CP (consistency) and AP (availability).

Splits the database across multiple nodes (shards), where each node stores a subset of the data. It lets you scale storage and query throughput horizontally beyond the limits of a single machine.

Sharding strategies:

  • Vertical partitioning: different tables on different servers. Simple, but limits the growth of individual tables.
  • Horizontal (range-based): users with id 1–1M on shard A, 1M–2M on B. Good for range queries, but can create hot spots if the distribution is uneven.
  • Hash-based: shard = hash(user_id) % N. Uniform distribution, avoids hot spots. Makes range queries harder. Consistent hashing minimizes data remapping when nodes are added or removed.
  • Directory-based: a lookup table maps key β†’ shard. Flexible (lets you move data without rehashing), but the table becomes a bottleneck and a single point of failure.

Classic problem: queries that span shards (cross-shard joins) are extremely expensive. Choose the sharding key based on the most frequent access patterns. If 90% of queries are by user_id, use user_id as the sharding key.

Stores computed results or frequently accessed data in memory (much faster than disk) to avoid recomputation or database I/O. It is one of the highest-impact optimizations in read-heavy systems.

Eviction policies (when memory is full):

  • LRU (Least Recently Used): removes the item accessed longest ago. It is the default β€” it works well for most workloads.
  • LFU (Least Frequently Used): removes the least frequently accessed item. Better when data popularity is stable over time.
  • TTL (Time-To-Live): each item has an expiration time. Essential for time-sensitive data (e.g. prices, sessions).

Write strategies:

  • Write-through: writes to the cache and the database at the same time. Guaranteed consistency, but higher write latency.
  • Write-back (write-behind): writes to the cache immediately and propagates to the database asynchronously. Higher throughput, but risk of data loss if the cache fails.
  • Write-around: writes straight to the database, bypassing the cache. The cache is only populated on the next read. Useful when written data is rarely read right afterwards.

Classic problems: Cache stampede (many keys expiring at the same time cause a flood of database queries β€” use probabilistic expiration or a mutex lock), cold start (empty cache after a deploy β€” use warm-up scripts), cache poisoning (invalid data in the cache).

Instead of processing everything synchronously inside the user's request, you enqueue the heavy work for asynchronous execution. The user gets a fast response (an acknowledgment), and workers do the processing in the background.

Classic use cases:

  • Sending email/SMS after sign-up
  • Generating an image thumbnail after upload
  • Payment processing with automatic retry
  • Indexing content for a search engine
  • Push notifications to mobile devices

Tools and tradeoffs:

  • RabbitMQ: AMQP, delivery guarantees (at-least-once), complex routing via exchange/binding. Ideal for workloads that need delivery guarantees.
  • Apache Kafka: distributed log, persistent message retention, replay capability, very high throughput (millions of msg/s). Ideal for event streaming, auditing, multiple independent consumers.
  • Amazon SQS: managed, zero ops, SQS Standard (at-least-once, unordered) or FIFO (exactly-once, ordered). Ideal if you want simplicity on AWS.

The producer-consumer pattern decouples services, absorbs traffic spikes (the producer can be much faster than the consumer for short periods) and enables automatic retry when a worker fails.

Three fundamental metrics to understand and size the network capacity of a distributed system:

Bandwidth
cap.
Theoretical maximum capacity of the channel in bits/second. It is the ceiling β€” the most the link can carry.
real
Throughput
Actual rate of successful transfer. Always ≀ bandwidth. Reduced by congestion, retransmissions and overhead.
RTT
Latency
Time for a packet to go from A to B (one-way) or A→B→A (Round-Trip Time). Made up of propagation + transmission + processing + queuing delay.

Components of latency:

  • Propagation delay: speed of light Γ— distance. SΓ£o Paulo β†’ New York β‰ˆ 100ms minimum.
  • Transmission delay: packet size Γ· bandwidth. A 1GB file over a 100Mbps link takes ~80 seconds.
  • Processing delay: time routers take to process the header and decide the next hop.
  • Queuing delay: time spent waiting in the buffers of congested routers. Variable and unpredictable.

Rule of thumb: optimizing throughput (batching, compression) can increase latency. Optimizing latency (smaller requests, persistent connections) can reduce throughput. Understand which one is more critical for your system before optimizing.

A programming paradigm for processing large datasets in parallel on a cluster. The programmer only defines the Map and Reduce functions; the framework takes care of distribution, parallelism, fault tolerance and the shuffle.

The flow of the two phases:

  • Map: each worker reads a chunk of the input data and emits (key, value) pairs. E.g.: for word count, each line β†’ emits (word, 1) for every word found.
  • Shuffle & Sort: the framework automatically groups all values emitted for the same key and sends them to the same reducer.
  • Reduce: the reducer receives (key, [v1, v2, v3, ...]) and produces the final result. E.g.: (word, [1,1,1]) β†’ (word, 3).

The framework takes care of: distributing data among workers, parallel execution, automatic re-execution of failed tasks (fault tolerance), shuffle/sort between Map and Reduce, and monitoring.

Implementations: Hadoop MapReduce (batch, disk I/O β€” robust but slow), Apache Spark (in-memory β€” 10-100x faster for iterative algorithms such as ML, streaming via Spark Streaming).

When NOT to use it: real-time queries with low-latency requirements, ACID transactions that need strong consistency, caching systems or simple lookups. MapReduce is for batch processing of large volumes.

# Pseudocode β€” Word Count in MapReduce
def map(document_id, text):
    for word in text.split():
        emit(word, 1)           # emits (key, value)

def reduce(word, counts):       # receives (key, [v1, v2, ...])
    emit(word, sum(counts))     # aggregates and emits the final result

The problem: with shard = hash(key) % N, changing N (adding a node or losing one) remaps almost every key. In a distributed cache this amounts to emptying the whole cache at once β€” and all traffic falls on the database.

The solution: nodes and keys are placed on a hash ring (e.g. 0 … 2Β³Β²βˆ’1). Each key belongs to the first node found going clockwise. When a node is added or removed, only the keys in the neighboring range change owner β€” on average K/N keys.

  • Virtual nodes: each physical node takes several positions on the ring, which evens out the distribution and lets you give more weight to bigger machines.
  • Replication: the key is also stored on the next Rβˆ’1 nodes of the ring (the Dynamo and Cassandra model).
  • Where it shows up: Cassandra, DynamoDB, Riak, Memcached clients (ketama), Nginx (hash $request_uri consistent;).

Replicating data increases availability and read capacity, but forces you to choose what the client may see while the replicas converge.

Topologies:

  • Leader-follower (primary-replica): writes go to the leader, reads scale out on the replicas. With asynchronous replication there is replication lag β€” a read on a replica may return stale data.
  • Multi-leader: writes accepted in several regions. Requires conflict resolution (last-write-wins, CRDTs, merge in the application).
  • Leaderless (quorum): N replicas; a write is acknowledged by W and a read queries R. If R + W > N, every read hits at least one replica with the latest acknowledged write. E.g.: N=3, W=2, R=2.

Consistency models (strongest to weakest): linearizability (strong) β†’ read-your-writes β†’ monotonic reads β†’ eventual consistency.

Practical trick: to guarantee read-your-writes without global strong consistency, read from the leader for a few seconds after the user's own write (e.g. after editing their profile).

A CDN (Content Delivery Network) is a network of edge servers that keep copies of content close to the user. It reduces latency (the RTT to the edge is much smaller than to the origin) and takes load off the origin.

  • Pull CDN: the edge fetches from the origin on the first access (cache miss) and stores it according to Cache-Control/TTL. Simple, the default in most cases.
  • Push CDN: you publish the content to the CDN explicitly. Useful for large, rarely changing files.
  • Invalidation: prefer versioning the file name (app.3f9a1c.js) over invalidating the cache β€” invalidation is slow and expensive.
  • Beyond static files: TLS termination, DDoS/WAF protection, edge compute and caching of dynamic responses with a short TTL.

Examples: Amazon CloudFront, Cloudflare, Akamai, Fastly.

Limits how many requests a client (user, API key, IP) can make within a time window. It protects against abuse and accidental spikes, ensures fair use and controls cost. The standard response is HTTP 429 Too Many Requests with a Retry-After header.

Algorithms:

  • Token bucket: tokens are refilled at a fixed rate; each request consumes one. Allows bursts up to the bucket size. The most common choice for APIs.
  • Leaky bucket: requests enter a queue that is drained at a constant rate. Smooths traffic (it is the model behind Nginx's limit_req).
  • Fixed window counter: one counter per window (e.g. per minute). Simple, but allows up to 2Γ— the limit at the window boundary.
  • Sliding window (log or counter): more accurate than the fixed window, at a higher memory/computation cost.

Distributed: with several gateway replicas, the counter must be shared β€” typically Redis (INCR + EXPIRE, or a Lua script for atomicity). Also decide what happens if Redis goes down: fail-open (let traffic through) or fail-closed (block it).

API Gateway: the single entry point of the system. It concentrates cross-cutting concerns β€” routing, authentication (JWT validation), rate limiting, TLS, CORS, response aggregation β€” so internal services don't have to repeat them.

  • Communication: synchronous (REST/gRPC) when the client needs the answer now; asynchronous (events on a queue) when the work can happen later.
  • Database per service: each service owns its data. Consistency across services via Saga (orchestrated or choreographed) and the Outbox pattern to publish events reliably.
  • Service discovery: the orchestrator's DNS (Kubernetes Services) or a registry (Eureka, Consul).

A tradeoff that impresses in interviews: knowing when not to use microservices. Small team and a still-unstable domain β†’ start with a modular monolith and extract services when there is a concrete reason (independent scaling, independent deploys, separate teams).

In distributed systems, partial failures are the rule. These patterns keep the failure of one dependency from bringing down the whole system (cascading failure).

  • Timeout: every remote call needs one. Without a timeout, a slow dependency exhausts the threads of its callers.
  • Retry with exponential backoff + jitter: only for transient errors and idempotent operations. Jitter keeps all clients from retrying at the same time (thundering herd).
  • Circuit breaker: closed β†’ open (fails fast, without calling the dependency) β†’ half-open (tests whether it is back).
  • Bulkhead: resource pools isolated per dependency, so that one cannot consume all the connections.
  • Idempotency key: the client sends a unique identifier; the server deduplicates repetitions of the same operation.
  • Fallback / graceful degradation: a degraded response (cache, default value) instead of an error.
  • Load shedding: reject part of the traffic early when saturated, instead of degrading every request.

Designing the system is not enough: you need to know when and where it is failing. Bringing up observability at the end of the interview shows production maturity.

  • Logs: what happened, event by event. Structured (JSON) and with a trace id.
  • Metrics: how much and how often. The RED method for services (Rate, Errors, Duration) and USE for resources (Utilization, Saturation, Errors).
  • Traces: where the time was spent in a request that crosses several services.

SLI / SLO / SLA: the SLI is the measurement (e.g. % of requests with latency < 300 ms); the SLO is the internal target (e.g. 99.9% over 28 days); the SLA is the contract with the customer, with a penalty. The error budget (1 βˆ’ SLO) defines how much risk you can spend on deploys and experiments.

Use percentiles, not averages: p50, p95 and p99. The average hides the tail β€” and the tail is where the unhappy users are.


Worked Example: URL Shortener

The 5 steps applied to the system in the diagram above β€” the outline of a ~45-minute answer.

1
Scope the Problem Functional: shorten a long URL, redirect the short code to the original URL, custom alias and expiration (optional), click counting. Out of scope: user accounts and link editing. Non-functional: low-latency redirect (p99 < 100 ms), high availability and codes that are not trivial to guess.
2
Make Reasonable Assumptions 100M new URLs/month β†’ ~40 writes/s. A 100:1 read:write ratio β†’ ~4,000 redirects/s on average (peak ~10k/s). Storage over 5 years: 6B records Γ— ~500 B β‰ˆ 3 TB. Cache: ~350M redirects/day; keeping the 20% most accessed β‰ˆ 35 GB of RAM β€” fits in a small Redis cluster.
3
Draw the Major Components API: POST /api/v1/urls {longUrl, alias?, expiresAt?} β†’ 201 {shortUrl} and GET /{code} β†’ 301/302 Location: longUrl. Model: urls(code PK, long_url, created_at, expires_at). Components: load balancer β†’ stateless app servers β†’ database.
4
Identify the Key Issues (a) Generating unique codes without collisions and without expensive coordination between servers. (b) The database takes all the read traffic. (c) Counting clicks synchronously would slow down the redirect.
5
Redesign for the Key Issues Cache-aside in Redis for the hot codes; read replicas on the database; sharding by hash of the code only if the volume requires it. Each click becomes an event on a queue (Kafka/SQS) consumed by workers that write to the analytics database β€” the redirect does not wait for it.

How to generate the short code?

Option A

Hash + truncate

Hash (MD5/SHA-256) of the URL, first 7 characters in base62. Deterministic: the same URL produces the same code. Con: collisions must be detected and resolved (re-hash with a salt).

Option B

Counter + base62

A unique ID (sequence, Snowflake or ranges pre-allocated per server) converted to base62. No collisions. Con: sequential codes are predictable and allow links to be enumerated.

Option C

Key Generation Service

A service pre-generates random keys and hands them out in batches to the app servers. No collisions on the request path, and unpredictable. Con: one more component to operate and replicate.

# base62: 62^7 β‰ˆ 3.5 trillion codes β€” plenty for 6B URLs
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def base62(n: int) -> str:
    s = ""
    while n > 0:
        n, r = divmod(n, 62)
        s = ALPHABET[r] + s
    return s or "0"
Classic tradeoff: 301 or 302?

301 (Moved Permanently) is cached by the browser: less load on the server, but subsequent clicks no longer go through you β€” the count is incomplete.
302 (Found) always goes through the server: accurate analytics, but more traffic. There is no right answer β€” choose and justify it with the requirements from step 1.


Practice Problems

Classic interview questions. Try to solve them with the 5 steps before opening the key points.

  • Choice of algorithm (token bucket vs. sliding window) and its justification.
  • State shared across gateway replicas (Redis) and atomicity (Lua script).
  • Rules per client/endpoint, a 429 response with Retry-After.
  • Fail-open vs. fail-closed if the counter storage goes down.
  • Fan-out on write (push to pre-computed timelines) vs. fan-out on read (build the timeline at read time).
  • Hybrid model: push for regular users, pull for celebrities with millions of followers.
  • Timeline in cache (a list of IDs in Redis), media in object storage + CDN.
  • Cursor-based pagination, not offset-based.
  • Persistent connections (WebSocket) and a service that knows which server each user is connected to.
  • Per-conversation ordering (a sequence number per conversation_id) β€” global ordering is not needed.
  • Delivery to offline users (store-and-forward) and delivery/read receipts.
  • Partitioning the message storage by conversation (e.g. Cassandra).
  • Multiple channels (push, e-mail, SMS) with separate queues and workers per channel.
  • Retries with backoff and idempotency so nobody is notified twice.
  • User preferences and opt-out, per-user rate limit, templates.
  • Integration with external providers (APNs, FCM, SES, Twilio) and what to do when they fail.
  • URL frontier: a prioritized queue partitioned by domain.
  • Politeness: respect robots.txt and limit requests per domain.
  • Deduplication: a Bloom filter for URLs already seen, a content hash for duplicate pages.
  • DNS cache, distributed page storage, spider-trap detection.
  • Frequent location updates (every few seconds) β€” a write-heavy system.
  • Geospatial index: geohash, quadtree or H3 for "nearby drivers".
  • Matching that guarantees a driver is never assigned to two rides (lock/strong consistency at that point).
  • Surge pricing, ETA and partitioning by region/city.
  • Partitioning with consistent hashing and virtual nodes.
  • Replication with a configurable quorum (N, R, W) and the consistency Γ— latency tradeoff.
  • Failure detection (gossip), hinted handoff and anti-entropy (Merkle trees).
  • Conflict resolution: vector clocks or last-write-wins.

Connection to the Course

Where each interview concept appears in the 2026.2 classes and hands-ons.

The hands-ons build, piece by piece, the same architecture as the diagram: gateway β†’ load balancer β†’ stateless services β†’ cache β†’ database β†’ queue β†’ observability. Use your project as a concrete example in the interview: "in my project, I put a shared Redis behind the gateway because each replica had an inconsistent local cache..."


Quiz

Test your knowledge with questions in the style of a real interview.

Question 1

Interview Checklist

Use it to assess your preparation before a System Design interview. Tick the items as you practice β€” your progress is saved in this browser.


References

Essential resources to go further β€” organized by type and relevance to technical interviews.

Books

Free Online Resources

GitHub β€” essential repository
System Design Primer
Donnemartin. The most complete free repository on system design. Includes flashcards, full examples, diagrams and a structured study guide. More than 280k stars on GitHub.
github.com/donnemartin/system-design-primer
Blog β€” real cases
High Scalability
A blog about how large systems (Twitter, Netflix, Amazon, Instagram) were built and scaled. Each post is a real architecture case study.
highscalability.com
YouTube β€” videos
ByteByteGo (Alex Xu)
A YouTube channel with animations and visual explanations of system design. Excellent for seeing how the components connect. Complements the System Design Interview book.
youtube.com/@ByteByteGo
Interactive course
Grokking Modern System Design Interview
Educative.io. A structured course with practice problems and step-by-step solutions. Covers the design of systems such as TinyURL, Instagram, Netflix and Uber.
educative.io
Tutorial
GeeksforGeeks β€” System Design Tutorial
A complete tutorial with an article for each concept: load balancing, caching, sharding, CAP theorem, consistent hashing, SQL vs NoSQL and more.
geeksforgeeks.org/system-design-tutorial
Practice
LeetCode β€” System Design Discussions
LeetCode forum with discussions of real system design questions asked in interviews at major tech companies (FAANG, high-growth startups).
leetcode.com/discuss/system-design
Online book β€” free
Site Reliability Engineering (Google)
Google's SRE books, available for free. The reference for SLOs, error budgets, monitoring and incident response β€” the "running in production" side of system design.
sre.google/books

Technical Articles & Papers

Tools to Practice


Coding Interview Prep Β· System Design & Scalability Study material β€” educational use