sharpbyte.dev

Design a URL shortener

A URL shortener takes a long URL and returns a shorter, unique alias that redirects to the original address. Services like TinyURL and Bitly became essential when social posts had character limits and when teams wanted cleaner, trackable links. In a system design interview, this question tests whether you can clarify requirements, estimate scale, separate read and write paths, and defend storage and caching choices under pressure.

This guide follows the same depth as a full interview walkthrough: requirements → capacity → architecture → database → APIs → deep dives on generation and redirect → scalability, availability, edge cases, and security.

Design prompt

Design a URL shortening service that maps long URLs to short aliases and redirects users when they open the short link.

Support optional custom aliases, link expiration, and click analytics if time allows.

What you should be able to do after reading:

1. Requirements gathering

Before drawing boxes, outline what the system must do and how well it must do it. Interviewers expect you to ask clarifying questions; below is a solid default scope.

1.1 Functional requirements

Usually out of scope unless asked: user accounts, link editing after creation, QR codes, A/B landing pages, browser extensions.

1.2 Non-functional requirements

Assumptions used in the math below: 1 million new short URLs per day; read:write ratio 100:1; peak traffic 10× average; average original URL length 100 characters; 7-character Base62 short codes.

2. Capacity estimation

Show your work. Rounded numbers are fine; the interviewer wants to see that you know which dimensions to estimate.

2.1 Throughput requirements

Average writes per second (WPS):

1,000,000 requests / 86,400 seconds ≈ 12 WPS (average)
Peak WPS ≈ 12 × 10 = 120 WPS

Read:write ratio 100:1 — for every URL created, expect ~100 redirects.

Average redirects/sec (RPS) ≈ 12 × 100 = 1,200 RPS
Peak RPS ≈ 120 × 100 = 12,000 RPS

Redirect traffic dominates. Size caches, connection pools, and read replicas for 12k RPS peak, not for 120 WPS writes.

2.2 Storage estimation

Per shortened URL, store at minimum:

FieldSize (approx.)
Short code (Base62)7 characters
Original URL100 characters (average)
Creation timestamp8 bytes
Expiration timestamp8 bytes
Click count (denormalized)4 bytes
Storage per URL ≈ 7 + 100 + 8 + 8 + 4 = 127 bytes (metadata only; indexes add overhead)

URLs per year = 1,000,000 × 365 = 365,000,000
Storage per year ≈ 365,000,000 × 127 bytes ≈ 46.4 GB

With indexes, replication, and analytics tables, plan for hundreds of GB over several years—still modest compared to video or log systems.

2.3 Bandwidth estimation

Assume each redirect response is ~500 bytes (status line, headers, small body if any).

Redirects per day ≈ 1,000,000 × 100 = 100,000,000
Read bandwidth ≈ 100,000,000 × 500 bytes ≈ 50 GB/day

Peak: 500 bytes × 12,000 RPS ≈ 6 MB/s egress

Write bandwidth for POST /shorten is negligible next to redirects.

2.4 Caching estimation

Read-heavy systems benefit enormously from cache. Use the 80-20 rule: ~20% of URLs drive ~80% of clicks.

Daily writes = 1M → cache top 20% of that day's hot set
Cache entries ≈ 1,000,000 × 0.2 × 127 bytes ≈ 25.4 MB

With a 90% cache hit ratio, only ~10% of redirects hit the database:

DB redirect load ≈ 1,200 × 0.10 ≈ 120 RPS (average)

That is comfortable for a sharded SQL or NoSQL store with read replicas. Without cache, every redirect hits disk—unacceptable at 12k RPS peak.

2.5 Infrastructure sizing (starting point)

LayerSizing (initial)Rationale
API / redirect servers4–6 instances behind LB, ~200–300 RPS eachStateless; scale horizontally
DatabaseDistributed store, 10–20 nodes or managed equivalentBillions of key lookups over lifetime
CacheRedis cluster, 3–4 nodesSub-ms lookups; replicate for HA
Analytics queueKafka / SQS if analytics in scopeDecouple clicks from redirect latency

3. High-level design

At a high level, the system needs these components:

flowchart TB
  subgraph clients [Clients]
    U[Browser / mobile / API]
  end
  subgraph edge [Edge]
    LB[Load balancer]
    RL[Rate limiter]
  end
  subgraph app [Application tier]
    APP[App servers]
    GEN[URL generation]
    REDIR[Redirection]
  end
  subgraph data [Data tier]
    CACHE[(Redis cache)]
    DB[(Database)]
    MQ[Message queue]
    WH[(Analytics warehouse)]
  end
  U --> LB --> RL --> APP
  APP --> GEN --> DB
  APP --> REDIR
  REDIR --> CACHE
  REDIR --> DB
  REDIR --> MQ --> WH
  CACHE -.-> DB
    

Request flows (two paths)

Shorten (write path): validate URL → generate or validate code → insert row → return short URL. Latency budget: tens of ms; not as critical as redirect.

Redirect (read path): extract code → cache lookup → DB on miss → HTTP 301/302 → async analytics. Latency budget: single-digit ms on cache hit.

sequenceDiagram
  participant C as Client
  participant A as App server
  participant G as URL generator
  participant D as Database
  C->>A: POST /shorten long_url
  A->>G: assign short_code
  G-->>A: abc12XY
  A->>D: INSERT mapping
  D-->>A: OK
  A-->>C: 201 short_url
    

4. Database design

4.1 SQL vs NoSQL

Both are defensible in interviews. Choose based on access pattern and ops comfort.

FactorFavors SQL (PostgreSQL)Favors NoSQL (DynamoDB / Cassandra)
Access patternPoint lookup by short_code; UNIQUE constraintsBillions of simple key-value gets
Custom alias + transactionsNatural fitConditional writes; design carefully
Scale-outRead replicas + sharding by keyBuilt-in partitioning
Analytics joinsEasier with SQL warehouse ETLStream to warehouse separately

Interview default: SQL primary + read replicas + Redis cache. Mention DynamoDB if the interviewer wants hyperscale with minimal ops.

4.2 Database schema

Two logical tables: URL mappings and users (if accounts exist). For anonymous shorten-only scope, the users table is optional but good to mention.

URL mappings

CREATE TABLE url_mappings (
  id              BIGSERIAL PRIMARY KEY,
  short_code      VARCHAR(16) NOT NULL,
  long_url        TEXT NOT NULL,
  long_url_hash   CHAR(64),              -- SHA-256 for dedup lookups
  user_id         BIGINT REFERENCES users(id),
  is_custom_alias BOOLEAN DEFAULT FALSE,
  expires_at      TIMESTAMPTZ,
  click_count     BIGINT DEFAULT 0,      -- optional denormalized counter
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  is_active       BOOLEAN NOT NULL DEFAULT TRUE,
  UNIQUE (short_code)
);

CREATE INDEX idx_url_long_hash ON url_mappings (long_url_hash)
  WHERE is_active = TRUE;

Users (optional)

CREATE TABLE users (
  id         BIGSERIAL PRIMARY KEY,
  email      VARCHAR(255) UNIQUE NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Click events (analytics)

CREATE TABLE click_events (
  id          BIGSERIAL PRIMARY KEY,
  short_code  VARCHAR(16) NOT NULL,
  clicked_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  referrer    TEXT,
  country     CHAR(2),
  user_agent  TEXT
);

At scale, raw click events often land in Kafka and aggregate in ClickHouse or BigQuery instead of growing unbounded in OLTP.

5. API design

REST-style endpoints that are easy to explain on a whiteboard.

5.1 URL shortening API

POST /shorten

Request body:

{
  "long_url": "https://www.example.com/some/very/long/url",
  "custom_alias": "my-link",           // optional
  "expires_at": "2027-12-31T23:59:59Z" // optional
}

Response 201 Created:

{
  "short_url": "https://go.sharp/my-link",
  "short_code": "my-link",
  "long_url": "https://www.example.com/some/very/long/url",
  "expires_at": "2027-12-31T23:59:59Z"
}

Errors: 400 invalid URL · 409 alias taken · 429 rate limited.

5.2 URL redirection API

GET /{short_url_key}

Response (301 example — permanent redirect):

HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/some/very/long/url

Use 302 Found when you need accurate per-click analytics or may change the destination later. Use 301 when the mapping is permanent and you want browsers/CDNs to cache the redirect (fewer origin hits, under-counted clicks).

404 Not Found — unknown code · 410 Gone — expired link.

5.3 Analytics API (optional)

GET /api/v1/urls/{short_code}/stats

{
  "short_code": "abc12XY",
  "total_clicks": 48291,
  "clicks_by_day": [ ... ]
}

6. Diving deep into key components

6.1 URL generator service

The generator must produce a short, unique, URL-safe key. Compare approaches on length, scalability, and collisions.

Approach 1: Hashing and encoding

Hash the long URL (MD5 or SHA-256), take a portion of the digest, convert to decimal, then Base62-encode. Base62 uses A–Z, a–z, 0–9 (62 characters)—URL-friendly and dense.

A 7-character Base62 string supports 6273.5 trillion unique codes.

Example workflow (illustrative):

  1. Long URL: https://www.example.com/some/very/long/url/that/needs/to/be/shortened
  2. MD5 hash (hex): 1b3aabf5266b0f178f52e45f4bb430eb
  3. Take first 6 bytes: 1b3aabf5266b → decimal 47770830013755
  4. Base62 encode → short code e.g. DZFbb43 (length ~7 depends on value)

Issues with hash-only:

Collision resolution strategies:

Approach 2: Unique ID generation

Assign each URL a monotonic numeric ID (1, 2, 3, …), then Base62-encode the ID. No collisions if the ID source is authoritative.

Considerations:

What to say in the interview: “I’d use Base62 of a distributed unique ID for default links—collision-free and fast. I’d use hash+retry only if product requires deterministic shortening. Custom aliases are a separate namespace with a UNIQUE constraint.”

Custom aliasing

Link expiration

6.2 Redirection service

When a user opens a short link, the service must resolve the code and issue an HTTP redirect—fast.

Example workflow:

  1. User opens https://go.sharp/abc123.
  2. Service extracts abc123.
  3. Check Redis for url:abc123 → if hit, read long URL.
  4. On miss, query database (read replica), then SET cache with TTL.
  5. Verify not expired; return 301/302 with Location: long_url.
  6. Publish click event to queue (non-blocking).
sequenceDiagram
  participant U as User browser
  participant R as Redirection service
  participant C as Redis
  participant D as Database
  participant Q as Analytics queue
  U->>R: GET /abc123
  R->>C: GET url:abc123
  alt cache hit
    C-->>R: long_url
  else cache miss
    R->>D: SELECT long_url WHERE short_code
    D-->>R: row
    R->>C: SET url:abc123 TTL 24h
  end
  R->>Q: click event async
  R-->>U: 302 Location long_url
    

Caching for performance

Redis (or Memcached) in front of the database cuts p99 latency and protects the DB during viral links.

6.3 Analytics service (optional)

Never block redirect on analytics.

flowchart LR
  REDIR[Redirect service] --> Q[Kafka]
  Q --> RT[Real-time counters]
  Q --> BATCH[Batch ETL]
  BATCH --> WH[(Data warehouse)]
    

7. Addressing key issues and bottlenecks

7.1 Scalability

API layer

Run stateless app servers behind a load balancer. Scale out when CPU or connection count rises. Separate redirect-only pools from shorten API if shorten traffic spikes during campaigns (optional optimization).

Database sharding

When one node is not enough, shard by short_code.

StrategyHowProsCons
Range-based IDs 1–1M on shard A, 1M+1–2M on shard B Simple with auto-increment IDs Hot last shard; uneven growth
Hash-based hash(short_code) % N Even spread Resharding painful without consistent hashing

Mention consistent hashing when adding shards without massive data movement.

Caching

Redis cluster with replication; cache the mapping, not the full HTTP response (destinations differ in headers). Monitor hit ratio—if it drops below ~85%, add memory or tune TTL.

7.2 Availability

7.3 Handling edge cases

ScenarioBehavior
Expired URLHTTP 410 Gone with friendly page; do not redirect
Non-existent codeHTTP 404 Not Found
Hash or alias collisionDetect on create; retry generation or reject with 409
Duplicate long URLReturn existing short URL (dedup) or create new—state product choice
Malicious long URLBlock at shorten time via Safe Browsing / internal denylist
Redirect loopsReject shortening a URL that points to your own domain (optional)

7.4 Security

8. Tradeoffs recap (whiteboard close)

TopicCommon choiceOne-line why
Short code algorithmBase62(unique ID)No collisions; predictable length
Primary storeSQL or DynamoDBPoint lookups by short_code at huge scale
Redirect code302 for analytics; 301 for permanenceProduct depends on measurement vs cache
CacheRedis cache-aside90%+ hit ratio → DB sees ~120 RPS not 1,200
AnalyticsAsync queueRedirect path stays milliseconds

9. How to present this in 35–45 minutes

  1. 5 min — requirements + assumptions (1M writes/day, 100:1 read ratio).
  2. 8 min — capacity: WPS, RPS, 46 GB/year, cache 25 MB hot set, 120 RPS to DB with 90% cache hit.
  3. 7 min — diagram: LB, app, DB, Redis, optional Kafka.
  4. 10 min — APIs + schema; Base62 ID vs hash; custom alias + expiration.
  5. 8 min — redirect + cache sequence; 301 vs 302; sharding and security if asked.
  6. 5 min — tradeoffs and future work (multi-region, custom domains).

The one line to remember

A URL shortener is a read-optimized key-value lookup: generate a unique short code once, cache short_code → long_url aggressively, redirect in one HTTP hop, and record analytics off the critical path.