Case Study: Designing a URL Shortener

A full worked example — requirements, capacity estimation, data model, and architecture.

This case study ties together every concept from this track — scale estimation, caching, database design, and CAP trade-offs — into one worked design, the way a real system design interview expects.

1. Clarify requirements

Functional:

  • Given a long URL, generate a short URL (e.g., noa.link/aZ9kLQ).
  • Visiting the short URL redirects (HTTP 301/302) to the original long URL.
  • Optionally: custom aliases, expiration dates, click analytics.

Non-functional:

  • Extremely read-heavy (every redirect is a read; shortening is a write) — assume a 100:1 read:write ratio.
  • Redirects must be very low latency (users notice slow redirects immediately).
  • High availability — a shortener that's down breaks every link that uses it, everywhere.
  • Uniqueness — no two long URLs should silently collide on the same short code (or if they can, it must be handled deliberately).

2. Estimate scale

Plaintext
Assume: 100M new short URLs created per month, 100:1 read:write ratio

Writes/sec:  100,000,000 / (30 * 24 * 3600) ≈ 39 writes/sec
Reads/sec:   39 * 100 ≈ 3,900 reads/sec

Storage (assume 5-year retention):
  100M/month * 12 * 5 = 6,000,000,000 records
  ~500 bytes/record (URL + metadata) ≈ 3 TB total

Conclusion: this is a read-dominated system at moderate-but-real scale — caching in front of the database is clearly justified, and the database choice matters less than getting the read path fast.

3. API design

Plaintext
POST /api/shorten
  body: { "url": "https://example.com/a/very/long/path" }
  response: { "short_url": "https://noa.link/aZ9kLQ" }

GET /{short_code}
  response: HTTP 301 redirect to the original long URL

4. How to generate the short code

Option A — Base62 encode an auto-incrementing ID. A database (or distributed ID generator) hands out sequential integer IDs; encode each in base62 (a-z, A-Z, 0-9) to get a short, URL-safe string.

Plaintext
id = 125_100_524
base62(125100524) = "8xwZ2"

Simple and guarantees uniqueness by construction (no collision checking needed), but a centralized ID generator can become a bottleneck/single point of failure at very high write volume — mitigated with pre-allocated ID ranges per server, or a distributed ID scheme (e.g., Twitter's Snowflake).

Option B — Hash the long URL (e.g., MD5/SHA-256) and take the first 6–8 characters. Simpler infrastructure (no central counter), but requires a uniqueness check against existing codes and a collision-handling strategy (append a character and rehash, retry).

For this design, Option A is the better trade-off: no collision-handling complexity, and ID generation scales fine with pre-allocated ranges.

5. Data model

SQL
CREATE TABLE urls (
    id            BIGINT PRIMARY KEY,       -- source for the base62 short code
    long_url      TEXT NOT NULL,
    short_code    VARCHAR(10) UNIQUE NOT NULL,
    created_at    TIMESTAMP NOT NULL,
    expires_at    TIMESTAMP NULL
);

CREATE INDEX idx_urls_short_code ON urls (short_code);

A relational database is a perfectly reasonable choice here — the data model is simple, and a key-value store (DynamoDB, Cassandra) would also work well if global multi-region write scale became a requirement later.

6. Where caching fits

Given the 100:1 read:write ratio, a cache in front of the database is one of the highest-leverage decisions in this whole design:

Plaintext
GET /{short_code}
   1. check Redis for key "url:{short_code}"
   2. hit  -> redirect immediately (sub-millisecond)
   3. miss -> query the database, populate Redis with a TTL, then redirect

Popular short URLs (viral links) will have an extremely high cache hit rate, taking the vast majority of read traffic off the database entirely — exactly the cache-aside pattern from the caching lesson in this track.

7. Scaling the redirect path

  • CDN / edge caching: since a redirect response for a given short code rarely changes, it can even be cached at the CDN edge for anonymous traffic, serving redirects with no origin round-trip at all for hot links.
  • Read replicas: for cache misses, read replicas absorb read load without touching the primary, which stays focused on writes (new short URL creation).
  • Horizontal scaling of the API layer: the redirect service itself is stateless — it can scale horizontally behind a load balancer with no coordination needed between instances.

8. CAP trade-off for this system

A short URL that's briefly stale (e.g., redirecting to a version of the mapping that's a few seconds out of date right after creation) is essentially harmless — nobody notices. This system should clearly favor availability over strict consistency (AP-leaning): if a cache or replica is slightly behind, that's a completely acceptable trade for keeping redirects fast and always available, per the CAP/PACELC discussion earlier in this track.

9. Summary architecture

Plaintext
Client --> CDN (edge cache) --> Load Balancer --> API servers (stateless)
                                                        |
                                          cache-aside    |    writes
                                          +-------------+------------+
                                          v                          v
                                       Redis                 Primary DB
                                    (short_code -> URL)      (+ read replicas)

Common mistakes (in this specific design, and in interviews generally)

  • Reaching for a hash-based short code without a real collision-handling plan.
  • Forgetting to estimate scale first — the entire justification for caching and read replicas comes directly from the 100:1 read-heavy ratio.
  • Over-engineering the very first version with sharding before establishing that a single primary + replicas + cache can't keep up — at ~39 writes/sec, a single primary handles writes easily.

Interview questions

Q: Why is a Base62-encoded auto-incrementing ID usually preferred over hashing the URL? It guarantees uniqueness by construction with no collision-checking or retry logic needed, and produces predictably short codes. Hashing requires handling the (rare but real) possibility of two different URLs producing colliding short hashes.

Q: Where would you put a cache in this system, and why? In front of the database, keyed by short code, using the cache-aside pattern — because the read:write ratio is extremely lopsided (100:1), caching the hot redirect path removes the vast majority of load from the database with a small, well-understood component.

Q: Should this system favor consistency or availability, and why? Availability — a short URL redirect being briefly stale (serving a cached mapping a few seconds old) causes no real harm, while the system being unavailable breaks every link using it, everywhere, immediately. This maps directly to an AP-leaning design per the CAP theorem.