System Design Interview: How to Design a URL Shortener Like Bit.ly

System Design Interview: How to Design a URL Shortener Like Bit.ly

ScriptNexScriptNex
May 14, 2026
3 min read
2,637 views

The Problem Statement

Design a URL shortening service like Bit.ly that:

  • Takes a long URL and returns a short, unique URL

  • Redirects short URLs to the original long URL

  • Handles 100 million URLs per month (write-heavy)

  • Short URLs should be as short as possible

  • Supports analytics (click count, geographic data)

  • High availability with minimal latency


This is one of the most common system design interview questions. Let's build it step by step.


Step 1: API Design

Core Endpoints

POST /api/v1/shorten
Body: { "long_url": "https://example.com/very/long/path", "custom_alias": "mylink" }
Response: { "short_url": "https://sho.rt/abc123", "expires_at": "2026-01-01" }

GET /{short_code}
Response: 301 Redirect to long_url

GET /api/v1/stats/{short_code}
Response: { "clicks": 15420, "created_at": "...", "top_countries": [...] }

Why 301 vs 302 Redirect?

  • 301 (Permanent) — browser caches the redirect. Reduces server load but you lose analytics on repeat visits.
  • 302 (Temporary) — every visit hits your server. Better for analytics. Use this.

Step 2: Encoding Strategy

The core problem: how do you generate a short, unique code?

Option A: Base62 Encoding

Use characters [a-zA-Z0-9] = 62 characters.
LengthCombinationsEnough for
656.8 billionMost use cases
73.5 trillionPlanet-scale
With 7 characters, we can support 3.5 trillion unique URLs. More than enough.

Option B: MD5/SHA256 Hash + Truncate

Hash the long URL, take the first 7 characters. Problem: collisions. You need collision resolution.

Option C: Auto-Increment ID + Base62

Use a database auto-increment counter, then convert to base62:
CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def encode_base62(num):
if num == 0: return CHARSET[0]
result = []
while num > 0:
result.append(CHARSET[num % 62])
num //= 62
return ''.join(reversed(result))

ID 1000000 -> "4c92"

ID 56800235583 -> "zzzzzzz"

Best approach: Option C — simple, no collisions, predictable length.

Step 3: Database Schema

CREATE TABLE urls (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    short_code VARCHAR(10) UNIQUE NOT NULL,
    long_url TEXT NOT NULL,
    user_id BIGINT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    expires_at TIMESTAMP NULL,
    click_count BIGINT DEFAULT 0,
    INDEX idx_short_code (short_code),
    INDEX idx_user_id (user_id)
);

CREATE TABLE click_events (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
url_id BIGINT NOT NULL,
clicked_at TIMESTAMP DEFAULT NOW(),
ip_address VARCHAR(45),
user_agent TEXT,
country_code VARCHAR(2),
referrer TEXT,
INDEX idx_url_id (url_id),
INDEX idx_clicked_at (clicked_at)
);

Database Choice

  • URLs table: MySQL or PostgreSQL — structured data, needs ACID transactions
  • Click events: Append-only, write-heavy → Consider Cassandra or ClickHouse for analytics at scale

Step 4: Caching Layer

Most URL shorteners follow the 80/20 rule — 20% of URLs get 80% of traffic.

Cache Strategy

  • Use Redis as a read-through cache
  • Cache the mapping: short_code → long_url
  • TTL: 24 hours for hot URLs
  • Cache eviction: LRU (Least Recently Used)
Request flow:
  • User hits GET /abc123
  • Check Redis cache → HIT → redirect (< 1ms)
  • Cache MISS → query database → store in Redis → redirect
  • Expected cache hit ratio: 80–90% for popular URLs.

    Step 5: Rate Limiting

    Prevent abuse (spam URL creation, DDoS):

    • Anonymous users: 10 URLs per hour per IP
    • Authenticated users: 100 URLs per hour
    • Implementation: Token bucket algorithm with Redis
    def rate_limit(user_id, limit=100, window=3600):
        key = f"rate:{user_id}"
        current = redis.incr(key)
        if current == 1:
            redis.expire(key, window)
        return current <= limit

    Step 6: Scaling Architecture

    Read Path (High Volume)

    Client → CDN/Load Balancer → API Servers (stateless)
                                        ↓
                                  Redis Cache
                                        ↓ (miss)
                                  Read Replicas (MySQL)

    Write Path

    Client → Load Balancer → API Server → Primary DB (MySQL)
                                  ↓
                             ID Generator (Snowflake/Auto-increment)
                                  ↓
                             Async: Kafka → Analytics Pipeline

    Key Scaling Decisions

  • Horizontal scaling of API servers (stateless, behind load balancer)
  • Database sharding by short_code hash (range-based sharding)
  • Read replicas for read-heavy traffic
  • CDN for static redirects of extremely popular URLs

  • Step 7: High Availability

    • Multi-region deployment with DNS-based routing
    • Database replication across availability zones
    • Graceful degradation — if analytics fails, redirects still work
    • Health checks — load balancer removes unhealthy instances

    Capacity Estimation

    MetricValue
    New URLs / month100 million
    Redirects / month10 billion (100:1 read:write)
    Redirects / second~3,800
    Storage / URL~500 bytes
    Storage / year~600 GB
    Cache size (20% hot)~120 GB → fits in Redis

    Interview Tips

  • Start with requirements — clarify functional and non-functional requirements
  • Do back-of-envelope math — show you can estimate scale
  • Design the API first — it frames the entire discussion
  • Discuss trade-offs — there's no "right" answer, only trade-offs
  • Address bottlenecks proactively — don't wait for the interviewer to ask
  • This question tests your ability to think at scale. Practice articulating your decisions clearly, and you'll nail it.

    Good luck with your interviews! 🎯
    ScriptNex

    ScriptNex

    @ScriptNex