0Pricing
DSA Interview Prep · Lesson

The System Design Interview Framework

Walk through the five-step RADIO framework (Requirements, API, Data, Infrastructure, Optimise) and practise applying it to a URL shortener.

The System Design Interview Framework is a free DSA Interview Prep lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DSA Interview Prep learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why System Design Matters in Interviews

System design interviews test your ability to think at scale — how would you design Twitter, YouTube, or a URL shortener for billions of users? Unlike coding problems with a single correct answer, system design is open-ended: you must make and justify trade-offs. Senior and staff-level roles devote 30–45 minutes to this round exclusively.

Interviewers evaluate whether you can clarify requirements, estimate load, propose a high-level architecture, dive into key components, and discuss trade-offs — all while communicating clearly. A structured framework prevents you from rambling and ensures you cover all critical dimensions.

# System design is not about a single correct answer.
# Interviewers look for:
eval_criteria = [
    'Ability to clarify requirements before designing',
    'Back-of-envelope capacity estimation',
    'High-level architecture with clear components',
    'Data modelling and storage choice',
    'Handling scalability (10x, 100x load)',
    'Trade-off discussion (consistency vs availability, etc.)',
    'Communication: talking through decisions as you make them',
]
for c in eval_criteria:
    print('-', c)

The RADIO Framework Overview

The RADIO framework provides a repeatable 5-step structure for any system design interview:

  • R — Requirements: functional and non-functional
  • A — API design: what operations does the system expose?
  • D — Data model: what data is stored and how?
  • I — Infrastructure: high-level components (servers, queues, caches)
  • O — Optimise: bottlenecks, caching, sharding, replication

Always move through these steps in order, but return and refine earlier steps when new insights emerge. Spend roughly equal time on each phase. Never jump straight to drawing boxes without first clarifying requirements.

RADIO = {
    'R': 'Requirements — What must the system do? What scale?',
    'A': 'API         — Define endpoints/operations the system exposes',
    'D': 'Data Model  — Entities, schemas, storage types',
    'I': 'Infra       — High-level architecture: servers, queues, caches, CDN',
    'O': 'Optimise    — Identify and address bottlenecks, trade-offs',
}
for step, desc in RADIO.items():
    print(f'[{step}] {desc}')

print('\nTiming guide for a 45-min interview:')
print('  R: 5 min | A: 5 min | D: 10 min | I: 15 min | O: 10 min')

Step R: Clarify Requirements

Never start designing without clarifying requirements. Ask about functional requirements (what the system does) and non-functional requirements (scale, latency, availability). For a URL shortener:

  • Functional: shorten a URL, redirect to original URL, optionally support custom aliases and expiry
  • Non-functional: how many URLs per day? Read-heavy or write-heavy? Availability requirement (99.9% vs 99.99%)? Acceptable latency?

Stating assumptions explicitly shows maturity. Interviewers often give deliberately vague specs to see if you ask the right questions. Two minutes of clarification saves you from designing the wrong system.

# Requirements questions for URL Shortener:
functional = [
    'Shorten a given URL to a 7-character alias',
    'Redirect short URL to original URL',
    'Allow custom aliases (optional)',
    'URL expiry (optional)',
    'Analytics: click count per URL (optional)',
]
non_functional = [
    '100 million new URLs per day (write: ~1160/sec)',
    '10:1 read:write ratio => 11,600 redirects/sec',
    'Redirects must be < 100ms p99 latency',
    '99.99% availability (< 1 hr downtime/year)',
    'URLs must be globally accessible',
]
print('Functional:')
for f in functional: print(' -', f)
print('\nNon-functional:')
for nf in non_functional: print(' -', nf)

Step R: Back-of-Envelope Estimation

After requirements, estimate capacity. This shows you can reason about scale before proposing solutions. Key numbers to derive: requests per second (RPS), storage needed per day/year, bandwidth, and memory for caching.

Use round numbers and approximate freely. Interviewers care about the order of magnitude, not exact figures. Example for a URL shortener: 100M writes/day ÷ 86400 ≈ 1160 writes/sec. 10B reads/day ÷ 86400 ≈ 115K reads/sec. Each URL record ≈ 500 bytes: 100M × 500B = 50 GB/day, 18 TB/year.

# Back-of-envelope for URL Shortener
writes_per_day = 100_000_000        # 100 million URLs/day
read_write_ratio = 100              # 100:1 read/write
reads_per_day = writes_per_day * read_write_ratio
bytes_per_url = 500                 # url string + metadata
years_to_store = 5

print('=== Capacity Estimation ===')
print(f'Writes/sec:  {writes_per_day / 86400:.0f}')
print(f'Reads/sec:   {reads_per_day / 86400:,.0f}')
print(f'Storage/day: {writes_per_day * bytes_per_url / 1e9:.1f} GB')
print(f'Storage total ({years_to_store}y): {writes_per_day * bytes_per_url * 365 * years_to_store / 1e12:.1f} TB')

cache_hit_rate = 0.80
hot_urls = reads_per_day * (1 - cache_hit_rate)
print(f'\n80% cache hit rate: {cache_hit_rate*100}% of reads from cache')
print(f'DB reads/sec: {hot_urls / 86400:,.0f}')

Step A: API Design

Define the API surface — the operations the system exposes to clients and internal services. Clearly specify the HTTP method, endpoint path, request parameters, and response format. This anchors the rest of the design: everything else exists to implement these APIs.

For a URL shortener, the two core APIs are: (1) POST /shorten to create a short URL, (2) GET /{alias} to redirect. Optional: DELETE /{alias} to delete, GET /{alias}/stats for analytics. Specify response codes (201 Created, 301 Redirect, 404 Not Found).

# API Design for URL Shortener
apis = [
    {
        'method': 'POST',
        'path': '/api/v1/shorten',
        'request': '{"long_url": "https://...", "alias": "optional", "expires_at": "optional"}',
        'response': '201 Created: {"short_url": "https://short.ly/abc1234", "alias": "abc1234"}',
    },
    {
        'method': 'GET',
        'path': '/{alias}',
        'request': 'No body',
        'response': '301 Redirect to long_url (or 404 Not Found)',
    },
    {
        'method': 'GET',
        'path': '/api/v1/{alias}/stats',
        'request': 'Optional: date range query params',
        'response': '200 OK: {"clicks": 42000, "unique_visitors": 15000}',
    },
]
for api in apis:
    print(f'{api["method"]} {api["path"]}')
    print(f'  Request:  {api["request"]}')
    print(f'  Response: {api["response"]}')
    print()

Step D: Data Model

The data model defines what you store and how. Identify the core entities and their attributes. For a URL shortener: a urls table with alias (primary key), long_url, created_at, expires_at, and user_id. An optional clicks table for analytics.

Choosing the right storage type is critical: relational DB for structured data with complex queries; key-value store (Redis, DynamoDB) for O(1) alias lookups at scale; object store (S3) for large blobs. For a URL shortener, a key-value store keyed on alias is ideal for reads, with a relational DB for writes and management.

# Data model for URL Shortener

# Core table (PostgreSQL)
urls_schema = '''
CREATE TABLE urls (
    alias       VARCHAR(16) PRIMARY KEY,   -- e.g., 'abc1234'
    long_url    TEXT NOT NULL,
    user_id     UUID REFERENCES users(id),
    created_at  TIMESTAMP DEFAULT NOW(),
    expires_at  TIMESTAMP,
    click_count BIGINT DEFAULT 0
);
CREATE INDEX ON urls(user_id);
'''

# Cache layer (Redis) for hot reads
redis_schema = '''
alias  =>  long_url       # O(1) GET on cache hit
TTL = 24 hours (or until expiry)
Cache eviction: LRU
'''

print('PostgreSQL schema:')
print(urls_schema)
print('Redis cache:')
print(redis_schema)
print('Storage split: Redis for hot reads (~80%), PostgreSQL for writes and cold reads')

Step I: High-Level Infrastructure

Sketch the high-level infrastructure: which servers handle which responsibilities, how data flows between components, and what external services are used. For a URL shortener at scale:

  • Load balancer: distributes traffic to write and read service replicas
  • Write service: generates alias, validates uniqueness, writes to DB, invalidates cache
  • Read/redirect service: checks Redis cache first, falls back to DB on miss
  • Relational DB: source of truth (with read replicas)
  • Redis cluster: caches hot URL mappings for sub-millisecond reads
# ASCII architecture sketch
architecture = '''
         Clients
            |
       [Load Balancer]
       /             \\
  [Write API]    [Read/Redirect API]
      |                  |  |
  [Alias Gen]       [Redis Cache]
      |                  |
  [PostgreSQL]---->[Read Replicas]

Alias Generation:
- Base62 encoding of auto-incrementing ID
- 62^7 = 3.5 trillion unique URLs (enough for 5 years at 100M/day)
- OR random 7-char Base62 with collision check
'''
print(architecture)
print('Key design decisions:')
print('  - Read/Write split: separate services for scalability')
print('  - Cache-aside pattern: read from Redis, fallback to DB')
print('  - 301 vs 302 redirect: 301 cached by browser (less load), 302 always hits server (analytics)')

Step O: Optimise and Handle Bottlenecks

The optimise phase addresses bottlenecks and scales the system. For a URL shortener, key concerns are: redirect latency (put Redis close to users with CDN or edge caches), alias uniqueness at scale (use a central ticket server or hash with collision detection), and DB write bottleneck (batch writes or async writes with a queue).

Discuss trade-offs explicitly: 301 redirects reduce server load but lose analytics accuracy; 302 redirects are trackable but add latency. Caching long expiry reduces DB load but risks stale URLs. Showing you understand these tensions signals senior-level thinking.

# Key optimisation discussion points
optimisations = {
    'Read latency': [
        'Redis cluster in multiple regions (CDN-edge caching)',
        '301 redirect for non-tracked URLs (client caches)',
        '302 redirect when click analytics are needed',
    ],
    'Write throughput': [
        'Async writes: accept request, enqueue to Kafka, batch-commit to DB',
        'Ticket server (centralised ID generator) to avoid UUID collision',
        'Or: hash(user_id + timestamp + random) with retry on collision',
    ],
    'Availability': [
        'Multi-AZ PostgreSQL with automatic failover',
        'Redis Sentinel or Redis Cluster for HA cache',
        'Health checks + circuit breaker on each service',
    ],
    'Storage': [
        'Partition urls table by hash(alias) for horizontal scaling',
        'Archive expired URLs to cold storage (S3)',
    ],
}
for area, points in optimisations.items():
    print(f'{area}:')
    for p in points: print(f'  - {p}')
    print()

Alias Generation: Base62 Encoding

A core technical detail of URL shorteners is how to generate short, unique aliases. The standard approach: use an auto-incrementing integer ID from the database and encode it in Base62 (digits 0-9, letters a-z, A-Z). A 7-character Base62 string can represent 62^7 ≈ 3.5 trillion unique URLs — enough for decades at 100M per day.

This is collision-free (each ID is unique) and produces short, URL-safe strings. The ID→alias mapping is deterministic and reversible. The trade-off: sequential IDs create predictable aliases (security concern). Shuffle the Base62 alphabet or use a counter offset to reduce predictability.

import string

BASE62_CHARS = string.digits + string.ascii_lowercase + string.ascii_uppercase
BASE = 62

def encode_base62(num):
    if num == 0:
        return BASE62_CHARS[0]
    result = ''
    while num:
        result = BASE62_CHARS[num % BASE] + result
        num //= BASE
    return result

def decode_base62(s):
    result = 0
    for c in s:
        result = result * BASE + BASE62_CHARS.index(c)
    return result

# Generate aliases for IDs 1, 100, 1000, 10^9
for id_ in [1, 100, 1000, 1_000_000, 10**9, 3_521_614_606207]:
    alias = encode_base62(id_)
    decoded = decode_base62(alias)
    print(f'ID {id_:20,} => alias "{alias}" (len={len(alias)}) => decoded={decoded}')

Applying RADIO to Twitter Feed Design

Let us briefly apply RADIO to designing a Twitter-like news feed to show the framework generalises:

  • R: Users post tweets, follow others, and see a feed of tweets from people they follow. Scale: 300M users, 500M tweets/day, feed must load in <2s.
  • A: POST /tweets, GET /feed, GET /timeline/{user_id}
  • D: tweets table (id, user_id, content, created_at); follows table (follower_id, followee_id); feed cache per user
  • I: Fan-out service writes new tweets to follower feeds (precomputed); Cassandra for write-heavy follow/tweet tables; Redis for feed caches
# Fan-out on Write vs Fan-out on Read trade-off
fan_out_strategies = {
    'Fan-out on Write (Push)': {
        'How': 'When user A posts, immediately write to all followers feeds',
        'Pro': 'O(1) feed read — feed is precomputed',
        'Con': 'Celebrities with 10M followers => 10M writes per post; very slow write',
        'Best for': 'Users with few followers (regular users)',
    },
    'Fan-out on Read (Pull)': {
        'How': 'When user reads feed, query followees tweets and merge',
        'Pro': 'Writes are fast (one DB write per tweet)',
        'Con': 'Feed read is slow: must query all N followees',
        'Best for': 'Celebrity accounts (few reads per post)',
    },
    'Hybrid': {
        'How': 'Push for regular users, pull for celebrities',
        'Pro': 'Balances read and write cost',
        'Con': 'More complex implementation',
        'Best for': 'Production systems like Twitter',
    },
}
for strategy, details in fan_out_strategies.items():
    print(f'{strategy}:')
    for k, v in details.items(): print(f'  {k}: {v}')
    print()

Common Mistakes in System Design Interviews

Avoid these frequent mistakes that derail system design interviews:

  • Jumping to solutions: drawing boxes before clarifying requirements signals poor engineering habits
  • No estimation: designing without knowing the scale is guesswork
  • Overengineering: designing for 1 billion users when the prompt says 10,000 wastes interview time
  • No trade-offs: every choice has pros and cons; failing to mention them suggests shallow understanding
  • Silence: interviewers need to hear your thought process; narrate decisions as you make them
# Checklist: before you stop talking, verify you covered:
checklist = [
    '[ ] Asked clarifying questions about scale and constraints',
    '[ ] Made capacity estimates (RPS, storage, bandwidth)',
    '[ ] Defined the API surface clearly',
    '[ ] Described the data model and storage choices',
    '[ ] Drew a high-level architecture with named components',
    '[ ] Identified the main bottleneck and proposed a solution',
    '[ ] Discussed at least one trade-off explicitly',
    '[ ] Verified the design meets the stated requirements',
]
for item in checklist:
    print(item)

Quick Check

Test your understanding of Data Structures & Algorithms — Coding Interview Prep concepts from this lesson.

Lesson Recap

In this lesson you learned: the RADIO framework structures system design interviews into Requirements, API, Data Model, Infrastructure, and Optimise, always clarify functional and non-functional requirements and make capacity estimates before proposing a design, and discuss trade-offs explicitly — every design choice has pros and cons that interviewers expect you to articulate. Next up we explore how to choose between SQL and NoSQL storage engines based on access patterns, consistency, and scale.

Frequently asked questions

Is the “The System Design Interview Framework” lesson free?

Yes — the full text of “The System Design Interview Framework” is free to read here on the web, and the DSA Interview Prep course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DSA Interview Prep course, upgrade to CoddyKit PRO.

What will I learn in “The System Design Interview Framework”?

Walk through the five-step RADIO framework (Requirements, API, Data, Infrastructure, Optimise) and practise applying it to a URL shortener. You practise DSA Interview Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start DSA Interview Prep?

No prior experience is required. DSA Interview Prep on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “The System Design Interview Framework” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this DSA Interview Prep lesson?

Yes. Every DSA Interview Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. The System Design Interview Framework
  2. Scalable Data Storage: SQL vs NoSQL
  3. Caching, CDNs, and Load Balancing
  4. Design Rate Limiter and Design Twitter Feed
← Back to DSA Interview Prep