How to Approach System Design Interviews
A structured framework for system design interviews — from requirement gathering to detailed design, with real examples.
System design interviews test whether you can take an ambiguous, large-scale problem and produce a reasonable architecture under time pressure. There is no single correct answer. What matters is your process: how you break down the problem, make trade-offs, and communicate your reasoning.
The Four-Step Framework
Step 1: Clarify Requirements (3-5 minutes)
Never start designing before you understand what you are building. Separate functional requirements (what the system does) from non-functional requirements (scale, latency, availability, consistency).
Good clarifying questions: "How many daily active users?" "What is the read-to-write ratio?" "Do we need real-time updates?" "Is eventual consistency acceptable?"
Write down the agreed requirements before moving on.
Step 2: Back-of-the-Envelope Estimation (3-5 minutes)
Rough math grounds your design in reality. For a URL shortener with 100 million URLs per month: about 40 writes/second, 4,000 reads/second at 100:1 ratio, roughly 50 GB/month storage.
These numbers tell you whether a single database suffices or you need caching and sharding. For a deeper dive, check the Back of the Envelope Estimation chapter on CodeJeet.
Step 3: High-Level Design (10-15 minutes)
Sketch the major components: client, load balancer, application servers, database, cache. Add problem-specific components: message queues for async processing, CDN for static content, WebSocket servers for real-time.
Draw the request flow end to end. Keep it high level -- do not dive into schema or algorithms yet.
Step 4: Detailed Design (10-15 minutes)
The interviewer picks one or two components for a deep dive. Common topics:
Database choice and schema. SQL vs NoSQL depends on access patterns. A URL shortener maps well to a key-value store. A social network needs relational joins.
Scaling strategies. Horizontal scaling, database sharding, read replicas, caching layers.
Specific algorithms. For a URL shortener: base62 encoding, hashing with collision handling, or a pre-generated key service.
Failure handling. Redundancy, failover, retries with exponential backoff, circuit breakers.
Let's explore some of these detailed components with practical code examples.
Database Schema Example: URL Shortener
A simple schema for a URL shortener would store the mapping between a short key and the original long URL. While a key-value store like Redis or DynamoDB is ideal, you might start with a simple SQL table.
# Python using SQLAlchemy ORM example
from sqlalchemy import create_engine, Column, String, DateTime, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
import datetime
Base = declarative_base()
class ShortURL(Base):
__tablename__ = 'short_urls'
id = Column(Integer, primary_key=True)
# The short key, e.g., "a3fG7d"
short_key = Column(String(10), unique=True, index=True, nullable=False)
# The original long URL
long_url = Column(String(2048), nullable=False)
created_at = Column(DateTime, default=func.now())
# For analytics or expiration
expires_at = Column(DateTime, nullable=True)
click_count = Column(Integer, default=0)
def __repr__(self):
return f"<ShortURL(short_key='{self.short_key}', long_url='{self.long_url}')>"
# Example usage for creating a record
# new_url = ShortURL(short_key='abc123', long_url='https://www.example.com/very/long/path')
Specific Algorithm: Base62 Encoding for Short URLs
A common algorithm for generating short keys is to take a unique numerical ID (e.g., from the database) and encode it into a base62 string using characters [0-9a-zA-Z]. This is reversible and compact.
# Python implementation of Base62 encoding/decoding
import string
BASE62_ALPHABET = string.digits + string.ascii_letters # 0-9, a-z, A-Z
BASE = len(BASE62_ALPHABET)
def encode_base62(num: int) -> str:
"""Encode a positive integer into a Base62 string."""
if num == 0:
return BASE62_ALPHABET[0]
encoded = []
while num > 0:
num, remainder = divmod(num, BASE)
encoded.append(BASE62_ALPHABET[remainder])
return ''.join(reversed(encoded))
def decode_base62(encoded_str: str) -> int:
"""Decode a Base62 string back to the original integer."""
num = 0
for char in encoded_str:
num = num * BASE + BASE62_ALPHABET.index(char)
return num
# Example usage:
# id = 123456789
# short_key = encode_base62(id) # e.g., "8M0kX"
# original_id = decode_base62(short_key)
Failure Handling: Implementing a Simple Retry with Exponential Backoff
When a service call fails, a retry mechanism with exponential backoff can help handle transient failures gracefully. This is a common pattern for improving resilience.
# Python example of a retry decorator with exponential backoff
import time
import random
from functools import wraps
def retry_with_backoff(max_retries=5, base_delay=1, max_delay=30):
"""
Decorator that retries a function on failure with exponential backoff and jitter.
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
retries = 0
while retries < max_retries:
try:
return func(*args, **kwargs)
except Exception as e:
retries += 1
if retries == max_retries:
print(f"All {max_retries} retries failed. Last error: {e}")
raise # Re-raise the last exception
# Calculate delay with exponential backoff and jitter
delay = min(max_delay, base_delay * (2 ** (retries - 1)))
jitter = random.uniform(0, delay * 0.1) # Add 10% jitter
total_delay = delay + jitter
print(f"Attempt {retries} failed. Retrying in {total_delay:.2f} seconds. Error: {e}")
time.sleep(total_delay)
return None # Should not be reached
return wrapper
return decorator
# Example usage:
# @retry_with_backoff(max_retries=3, base_delay=1)
# def call_external_api(url):
# # ... make HTTP request
# pass
Common Mistakes
Jumping into components without requirements. An interviewer asking "design Twitter" might want the news feed, tweet publishing, or notification system. Clarify first.
Ignoring trade-offs. Every decision has a cost. If you choose eventual consistency, explain why strong consistency is unnecessary.
Overcomplicating the design. A URL shortener does not need Kafka and microservices on day one. Design for stated requirements, then mention what you would add as the system grows.
Not communicating. System design is a conversation. Explain your reasoning as you draw. Discuss options when you are unsure.
How to Practice
Study common questions: URL shortener, news feed, chat system, notification service. CodeJeet has a full system design curriculum with step-by-step chapters, including the System Design Framework chapter that expands on this approach.
Time yourself at 35-45 minutes. Practice with another person -- system design is interactive, and mock interviews force you to explain your thinking and handle follow-ups.
Practice Exercise: Sketch a High-Level Design for a News Feed
Let's apply the framework to a common problem: designing a news feed like Twitter or Facebook. After clarifying requirements (e.g., 10 million DAU, 100 posts per user per day on average, timeline must update in near real-time), you would move to a high-level sketch.
Core Components:
- Client Apps: Mobile and web clients.
- Load Balancers: Distribute incoming requests across multiple servers.
- API Servers: Handle HTTP requests (post a tweet, fetch feed, follow a user).
- Post Service: Manages the creation, storage, and retrieval of individual posts/tweets. It writes to a Post Database (likely sharded by user ID or post ID).
- Fanout Service: The critical component for news feed generation. Two primary approaches:
- Pull Model (Read-time): When a user requests their feed, the service queries the Post Database for posts from all users they follow, then ranks and merges them. Simple but slow for users with many follows.
- Push Model (Write-time): When a user posts, the service immediately pushes that post into the "inbox" (a dedicated cache or table) of all their followers. Fetching the feed is then a fast read of the user's own inbox. Fast for reads but heavy on writes for popular users.
- Hybrid Model: Often used in practice. Push for regular users, pull for celebrities/influencers with massive follower counts.
- Feed Cache: Stores pre-computed feed data for each user (especially in a push/hybrid model). Redis or Memcached are common choices.
- Graph Service (Social Graph): Manages the "follow" relationships—who follows whom. Stored in a graph database or a highly available key-value store.
- Message Queue (e.g., Kafka, RabbitMQ): Decouples services. For example, when a post is created, an event is published to a queue. The Fanout Service consumes this event to perform the push operation asynchronously.
Data Flow for Posting a Tweet (Push Model):
- Client -> Load Balancer -> API Server.
- API Server writes the post to the sharded Post Database.
- API Server publishes a "Post Created" event to the Message Queue.
- Fanout Service workers consume the event.
- Fanout Service queries the Graph Service to get the list of follower IDs.
- For each follower ID, the service inserts the post ID into the follower's sorted feed in the Feed Cache (e.g., a Redis Sorted Set by timestamp).
Data Flow for Fetching a Feed:
- Client requests the feed.
- API Server reads the user's pre-computed feed from the Feed Cache.
- If a cache miss (or for pull-model users), it queries the Post Database for posts from followed users, then ranks them.
- Returns the ranked list of post IDs.
- A separate request fetches the full post content from the Post Service for the IDs (a process called "hydration").
Practicing by drawing this flow and discussing the trade-offs between pull vs. push models is excellent preparation.