System Design

How would you design a URL-shortening service?

A URL shortener is the "Hello World" of system design interviews. Simple on the surface. But once you start thinking about scale, collision handling, anal...

8 Mar 2024

How would you design a URL-shortening service?

A URL shortener is the "Hello World" of system design interviews. Simple on the surface. But once you start thinking about scale, collision handling, analytics, and expiration — it gets interesting fast.

I use this problem in workshops because it touches hashing, caching, database selection, and horizontal scaling in a compact design.

Business Requirements

  • Short URL Generation — convert a long URL into a short, unique code. Support custom aliases ("mysite.co/launch-sale").
  • Redirection — when someone visits the short URL, redirect them to the original URL. Fast. Sub-100ms.
  • Analytics — track click counts, geographic distribution, referrer data, device types. This is where the real business value lives.
  • Link Management — let users view, edit, and delete their links. Support expiration dates.
  • High Availability — short URLs are shared in emails, social media, printed materials. If the service is down, every link is broken.

Technical Requirements

  • Scalable Database — store billions of URL mappings. Handle high read throughput (reads dominate — every click is a read, but URL creation is infrequent).
  • Efficient Shortening Algorithm — generate short, unique codes quickly. Handle collisions gracefully.
  • Caching — most clicks go to a small set of popular URLs. Cache these for sub-millisecond redirects.
  • Load Balancing — distribute traffic across multiple servers. No single point of failure.
  • Rate Limiting — prevent abuse (spam, phishing links). Limit URL creation per user/IP.

Initial High-Level Design

Screenshot 2024-03-08 at 5.02.13 PM.png

This first design has problems:

  • Single web server — single point of failure (SPOF).
  • No horizontal scaling.
  • Single database — won't handle 60TB of storage or 8,000 reads per second.

Improved Design

Screenshot 2024-03-08 at 5.06.37 PM.png

Add load balancers, multiple app servers, database sharding or replication, and a cache layer (Redis) in front of the database. Now you can scale each component independently.

The Shortening Algorithm: Base62 Encoding

The most common approach: generate a unique integer ID, then encode it in Base62.

Base62 uses [0-9a-zA-Z] — 62 characters. A 7-character code gives you 62^7 = 3.5 trillion possible URLs. That's plenty.

How it works:

  1. Generate a unique ID (auto-increment counter, or a distributed ID generator like Snowflake).
  2. Convert the ID to Base62.
  3. Use the result as the short code.

Example:

  • Unique ID: 12345
  • Base62 encoding: "3D7"
  • Short URL: https://sho.rt/3D7

For custom aliases, check uniqueness against the database. If the alias exists, reject the request.

The trade-off: auto-increment IDs are simple but predictable (users can guess other URLs by incrementing). Hash-based approaches (MD5/SHA256 truncated to 7 chars) are less predictable but require collision handling.

Database Selection

This is a read-heavy system. The read/write ratio is likely 100:1 or higher. Every click is a read. URL creation is comparatively rare.

Key-value stores (Redis, DynamoDB) are ideal for the core mapping: short_code → original_url. Fast lookups. Horizontal scaling. No complex queries needed.

Relational databases (PostgreSQL) work for user data, analytics, and link management where you need joins and transactions.

A common pattern: Redis as the primary lookup store (for speed), PostgreSQL as the source of truth (for durability and complex queries), with an async sync between them.

The Trade-Offs

Auto-increment IDs vs. hashes. Auto-increment is simple and collision-free but requires a centralized counter (bottleneck at scale) and is predictable. Hashes are distributed-friendly but need collision handling. UUIDs avoid collisions but produce longer codes. Choose based on your scale and security needs.

Caching vs. freshness. Cache short URL → long URL mappings aggressively. Most URLs never change after creation. But if you support URL editing or expiration, you need cache invalidation. For most shorteners, set a long TTL and invalidate on update.

Analytics: real-time vs. batch. Real-time click analytics add latency to the redirect path. Batch processing (log clicks, process later) keeps redirects fast. I'd log clicks to Kafka, process with a batch pipeline, and store aggregates in a time-series database.

Custom aliases: flexibility vs. abuse. Custom aliases are a great feature but open the door to squatting and abuse. Rate limit creation. Reserve common words. Implement a reporting mechanism.

The beauty of this problem is that it's simple enough to sketch in 10 minutes but deep enough to discuss for an hour. Every component — the ID generator, the cache layer, the analytics pipeline — has meaningful trade-offs worth exploring.