Question Breakdowns

URL Shortener

A worked system design: turn long URLs into short codes and redirect on lookup. Requirements, API, the short-code generation question, a read-heavy architecture, and the trade-offs that matter.


The URL shortener is the canonical starter problem — small enough to finish, rich enough to show real judgement. Take https://example.com/some/very/long/path and return short.ly/abc123; when someone visits the short link, redirect them to the original. Let's design it the way you would in an interview, applying the Core Concepts directly.

1. Requirements

Functional:

  • Create a short URL from a long one.
  • Redirect a short URL to its long original.
  • (Nice-to-have) custom aliases, expiry, click analytics.

Non-functional — this is where the design is decided:

  • Read-heavy. Links are created once and visited many times — assume ~100:1 reads:writes.
  • Low latency on redirects (it's in the user's critical path).
  • High availability — a dead redirect is a broken link everywhere it was shared.
  • Short codes should be short and not easily guessable.

2. Scale check

Say 100M new links/month. Using Numbers to Know: that's 100M / 2.5M ≈ 40 writes/sec, and at 100:1, ~4,000 reads/sec. Over 5 years, 6B links × ~500 bytes ≈ 3 TB. Takeaway: writes are trivial, reads want a cache, and the data fits comfortably on one database (with room to shard later). Nothing exotic needed — resist over-engineering.

3. API

POST /urls        { "url": "https://..." , "alias?": "..." }  → { "short": "short.ly/abc123" }
GET  /{code}      → 302 redirect to the long URL

The redirect returns 302 (or 301) with the long URL in the Location header. Prefer 302 if you want every visit to hit your server (for analytics); 301 is cached hard by browsers and is faster but you lose per-visit tracking.

4. Data model

One table is enough:

urls ( code PK, long_url, created_at, expires_at?, owner_id? )

The whole system is: write a row on create, look up by code on redirect. An index on the primary key code makes the lookup a log-n hit.

5. The interesting question: generating the code

This is the real design decision. Options:

  • Hash the URL (e.g. first 7 chars of a hash). Simple, but collisions happen — two different URLs can hash to the same prefix — so you must check-and-retry, and identical URLs collapse to one code (sometimes fine, sometimes not).
  • Random code. Generate a random 7-character base-62 string. 62^7 ≈ 3.5 trillion possibilities — plenty, and not guessable — but you must check for a collision on insert (rare, so a unique constraint + retry is fine).
  • Counter + base-62 encoding (recommended). Keep a global counter; each new link takes the next number and encodes it in base 62 (a–z A–Z 0–9). No collisions by construction, and codes stay short (62^7 covers trillions). The catch is the counter must be distributed — a single counter is a bottleneck and a single point of failure. The fix: hand each app server a range of ids from the counter (say 1,000 at a time) so it mints codes locally and only hits the counter occasionally.

The counter + base-62 approach is the clean interview answer: no collisions, short codes, and the range trick removes the bottleneck.

6. High-level design

create:   client → API → write row (code, url) → return short URL
redirect: client → API → cache check → hit? 302
                                      → miss? DB lookup, cache it, 302

Because it's read-heavy, a cache (Redis) in front of the lookup serves the hot links (recently/popular) from memory — most redirects never touch the database. Put a CDN / edge in front too if you can resolve popular codes at the edge. This is exactly the Scaling Reads toolbox: cache first, replicas if needed.

7. Deep dives an interviewer may push on

  • Make redirects fast: cache aggressively; a short link is immutable, so it caches forever (until expiry) with no invalidation worry.
  • Scaling storage: 3 TB fits one beefy database, but if needed, shard by code — each shard owns a slice of the keyspace, and a redirect routes straight to one shard.
  • Custom aliases: a user-supplied code is just an insert that fails on the unique constraint if taken.
  • Analytics: don't write a row per click synchronously — fire an event to a queue/stream and aggregate (see Scaling Writes), so tracking never slows the redirect.
  • Expiry: a TTL / expires_at column, with a background job (or lazy check on read) removing dead links.

What to emphasise

The signal here isn't complexity — it's restraint and the code-generation reasoning:

"It's read-heavy and small, so one database plus a Redis cache handles it; redirects are cache-first and the data caches forever since a short link is immutable. For codes I'd use a global counter encoded in base-62 so there are no collisions and codes stay short, handing each server a range of ids to avoid a counter bottleneck. Analytics go through a queue so they never slow the redirect."


Next up: Rate Limiter — a small design with a surprising amount of depth.