DEV Community

Otto
Otto

Posted on

System Design for Beginners: How to Design a URL Shortener in 2026

System Design for Beginners: How to Design a URL Shortener in 2026

System design interviews terrify most developers. The questions are open-ended, the scope is massive, and there's no single "right" answer.

But here's the secret: every system design question follows the same framework. Once you learn it with a simple example, you can apply it to anything.

We'll use the URL shortener — the most classic system design question — as our learning vehicle.

What Is System Design?

System design is the process of defining the architecture, components, modules, interfaces, and data flow for a system to satisfy given requirements.

In interviews, you're asked to design systems like:

  • URL shortener (bit.ly)
  • Twitter/X timeline
  • YouTube
  • Uber's ride dispatch
  • Netflix video streaming

The System Design Framework (6 Steps)

1. Clarify Requirements
2. Estimate Scale
3. Define the API
4. Design the Data Model
5. Draw the High-Level Architecture
6. Deep Dive into Components
Enter fullscreen mode Exit fullscreen mode

Let's apply each step to the URL shortener.

Step 1: Clarify Requirements

Functional requirements (what the system does):

  • Given a long URL, create a short URL (e.g., bit.ly/abc123)
  • Given a short URL, redirect to the original long URL
  • Short URLs should expire after a configurable time (optional)
  • Users can create custom short URLs (optional)

Non-functional requirements (how well it does it):

  • High availability: the system should be up 99.9% of the time
  • Low latency: redirects should happen in < 100ms
  • Durability: short URLs shouldn't disappear unexpectedly

Step 2: Estimate Scale

Asking about scale shows you think like a senior engineer:

Assumptions:
- 100M new URLs shortened per day
- Read/write ratio: 100:1 (100 redirects per 1 new URL)
- Average URL size: 100 bytes

Calculations:
- Write QPS: 100M / 86,400 sec ≈ 1,160 writes/sec
- Read QPS: 1,160 × 100 = 116,000 reads/sec
- Storage (5 years): 100M × 365 × 5 × 100 bytes ≈ 18 TB
Enter fullscreen mode Exit fullscreen mode

This tells us: read-heavy system, needs caching, moderate storage needs.

Step 3: Define the API

POST /api/v1/urls
Request: { "longUrl": "https://example.com/very/long/path", "expiresAt": "2027-01-01" }
Response: { "shortUrl": "https://bit.ly/abc123", "expiresAt": "2027-01-01" }

GET /{shortCode}
Response: 301 Redirect to longUrl

DELETE /api/v1/urls/{shortCode}
Response: 200 OK
Enter fullscreen mode Exit fullscreen mode

301 vs 302 redirect:

  • 301 (Permanent): browser caches the redirect → fewer requests to our servers → cheaper
  • 302 (Temporary): browser always asks our server → more accurate analytics

Choice depends on requirements.

Step 4: Design the Data Model

Simple schema:

CREATE TABLE urls (
  short_code  VARCHAR(7)    PRIMARY KEY,
  long_url    VARCHAR(2048) NOT NULL,
  created_at  TIMESTAMP     DEFAULT NOW(),
  expires_at  TIMESTAMP,
  user_id     BIGINT,       -- nullable for anonymous
  click_count BIGINT        DEFAULT 0
);

CREATE INDEX idx_short_code ON urls(short_code);
Enter fullscreen mode Exit fullscreen mode

The short_code is the key. How do we generate it?

Short Code Generation Strategies

Option 1: Hash + Truncate

import hashlib

def generate_short_code(long_url: str) -> str:
    hash_value = hashlib.md5(long_url.encode()).hexdigest()
    return hash_value[:7]  # Take first 7 chars of MD5 hash
Enter fullscreen mode Exit fullscreen mode

Problem: hash collisions (two URLs → same short code).

Option 2: Base62 Encoding of a Counter

BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def encode_base62(num: int) -> str:
    result = []
    while num > 0:
        result.append(BASE62[num % 62])
        num //= 62
    return ''.join(reversed(result)).zfill(7)
Enter fullscreen mode Exit fullscreen mode

Atomically increment a counter in the DB, encode it. Unique by design. ✅

Option 3: Pre-generated Keys (Key Generation Service)
Pre-generate millions of unique 7-char strings, store in a keys_available table. When needed, grab one and move to keys_used. Eliminates collision risk entirely.

Step 5: High-Level Architecture

[Client]
    │
    ▼
[Load Balancer]
    │
    ├─── [Write Service] ──── [Primary DB]
    │                              │
    └─── [Read Service]  ──── [Redis Cache] ──── [Read Replica DB]
Enter fullscreen mode Exit fullscreen mode

Write path:

  1. Client POSTs long URL
  2. Write service generates short code (Base62 counter or KGS)
  3. Saves to primary DB
  4. Returns short URL

Read path:

  1. Client GETs short code
  2. Read service checks Redis cache first (99% of requests should hit cache)
  3. Cache miss → query read replica DB
  4. Return 301/302 redirect

Step 6: Deep Dive — Caching Strategy

With 116K reads/sec, the database alone can't handle traffic. Redis is essential.

import redis

cache = redis.Redis(host='localhost', port=6379)
CACHE_TTL = 3600  # 1 hour

def get_long_url(short_code: str) -> str | None:
    # Try cache first
    cached = cache.get(f"url:{short_code}")
    if cached:
        return cached.decode()

    # Cache miss — query database
    long_url = db.query("SELECT long_url FROM urls WHERE short_code = %s", short_code)

    if long_url:
        cache.setex(f"url:{short_code}", CACHE_TTL, long_url)

    return long_url
Enter fullscreen mode Exit fullscreen mode

Cache eviction policy: Use LRU (Least Recently Used). 80/20 rule — 20% of URLs get 80% of traffic. Cache that 20%.

Common Follow-Up Questions

Q: How do you handle expired URLs?

  • Lazy deletion: check expiry on read, return 410 Gone if expired
  • Background job: scan and delete expired URLs nightly

Q: How do you prevent abuse?

  • Rate limiting per IP (Redis sliding window counter)
  • Block malicious URLs (Safe Browsing API)
  • Captcha for anonymous users

Q: How do you scale to 1B requests/day?

  • Add more read replicas
  • Geographic distribution (CDN for redirects)
  • Consistent hashing for distributed cache

Q: Custom short codes?

  • Check uniqueness before saving
  • Separate table for reserved/custom codes

What Interviewers Are Actually Evaluating

  1. Communication — Can you walk through your thinking clearly?
  2. Trade-offs — Do you understand why each decision was made?
  3. Scale awareness — Do you estimate before building?
  4. Depth — Can you go deep on at least one component when asked?

You don't need to have the perfect answer. You need to drive the conversation, make reasonable choices, and explain your reasoning.

Practice Problems (Increasing Difficulty)

  1. URL Shortener (this article) — Start here
  2. Pastebin (text sharing)
  3. Rate Limiter (classic API design)
  4. Twitter Timeline (fan-out problem)
  5. Design YouTube (video storage + streaming)
  6. Design Uber (real-time matching)

Managing freelance projects and clients while studying system design? Freelancer OS keeps everything organized — CRM, projects, income, all in Notion. €19 one-time.

Top comments (0)