Design a URL Shortener

Build a service that turns long web addresses into short links such as sho.rt/aZ3k9Q, and sends anyone who opens a short link to the original address.

Marketing teams paste links into ads, emails and printed QR codes, so a short link must keep working for years, and redirects must feel instant everywhere in the world. Some links go viral and get millions of clicks in an hour.

In 45 minutes, agree on requirements, size the system, sketch the API and data model, draw the architecture, then go deep on how you generate codes and how you keep redirects fast.

Sketch on the HLD board, write your answer in the Design doc, and size it with the Estimator. Start the mock interview from the timer for a 45-minute run.

Design Path · Core Building Blocks · problem 3 of 18

Ask before you design. Try answering each yourself, then compare.

  1. Should the same long URL always get the same short code?
    Typical answer

    Not required. Each request may get a fresh code; dedupe per user is a nice-to-have.

  2. Can users pick a custom alias, and can links expire?
    Typical answer

    Yes to both, as optional features. Default links never expire.

  3. How short must codes be, and which characters?
    Typical answer

    7 characters of letters and digits (base62).

  4. What scale should we plan for?
    Typical answer

    About 100 million new links a month and roughly 100 redirects per link.

  5. Do we need click analytics?
    Typical answer

    Yes: click counts per link, viewable within a few minutes. Not real-time to the second.

  6. Is a short delay before a new link works acceptable?
    Typical answer

    A second or two is fine; a broken link is not.

Work out the read:write ratio first. It tells you which path to optimise.
How many 7-character base62 codes exist? Is that enough for five years of links?
Hashing the long URL can collide. What if each app server could hand out unique numbers without asking anyone each time?
Redirects are tiny, repeated lookups by key. What sits in front of the database for that?

Requirements

Functional

  • Create a short link for a long URL, optionally with a custom alias and expiry.
  • Redirect a short link to its long URL.
  • Show click counts per link.

Non-functional

  • Redirect p99 under 50 ms, 99.99% availability for redirects.
  • Links are durable for at least 5 years.
  • Codes must not be easy to enumerate.

Out of scope

  • User accounts and billing.
  • Link previews and malware scanning (mention as follow-ups).

Back-of-the-envelope estimates

  • Writes: 100 M links / month ≈ 40 per second on average, about 120 at peak.
  • Reads: 100× writes ≈ 4,000 redirects per second, 12,000 at peak.
  • Storage: 6 B links over 5 years × ~500 bytes ≈ 3 TB, about 9 TB with three replicas.
  • Cache: caching the hottest 20% of a day's links needs only tens of GB, which fits a small Redis cluster.

The redirect path dominates: it is read-heavy, latency-sensitive and very cacheable.

API

POST /v1/links   {longUrl, alias?, expiresAt?}  -> 201 {code, shortUrl}
GET  /{code}                                  -> 302 Location: longUrl
GET  /v1/links/{code}/stats                   -> {clicks, lastClickAt}

Use 302 rather than 301 so browsers ask us on every click; that keeps analytics accurate and lets links be changed or disabled.

Data model

links(code PK, long_url, owner_id, created_at, expires_at) in a key-value or wide-column store, partitioned by code. Every read is a single-key lookup, so there are no joins and sharding is trivial.

link_clicks(code, day, count) is written by the analytics pipeline, not the redirect path.

High-level design

A client calls the load balancer, which spreads traffic across stateless URL services. On create, the service takes the next number from a block of IDs it reserved earlier from the key generation service, encodes it in base62, and writes the row. On redirect, it looks the code up in the cache first and falls back to the store on a miss, filling the cache on the way back. Every redirect also drops a small click event onto a stream, so analytics never slow down the redirect. A worker aggregates the events into per-link counts in an analytics database.

Reads and writes share the same services, but they barely touch the same resources: writes go to the key generation service and the store, while nearly all reads end at the cache.

Everything on the redirect path is stateless or replicated, so any single machine can fail without breaking links.

Open the HLD board and press Reference to see the architecture diagram.

Deep dives

Generating codes without collisions

Hashing the long URL and truncating it gives collisions that you have to detect and retry. Instead, keep a counter in a small, strongly consistent key generation service. Each app server reserves a block of, say, 10,000 numbers at a time and hands them out locally, so creating a link needs no network call almost every time. Base62-encoding the number gives the code. If codes must not be guessable, run the number through a reversible shuffle (a keyed permutation) before encoding.

Keeping redirects fast for viral links

Use cache-aside with a TTL: look in Redis, fall back to the store, then write the result back. A viral link becomes one hot key, so add a small in-process cache on each service instance for the top few thousand codes, and consider serving very hot links from the CDN edge. Negative caching (remembering that a code does not exist) stops scanners from hammering the database.

Counting clicks without slowing redirects

Never increment a counter in the store during a redirect. Emit an event to a stream instead and return immediately. A worker batches events per code and per minute and adds them to the analytics store, which keeps writes low even at peak. If the stream is briefly down, buffer events in memory and accept a small loss: clicks are statistics, not money.

Failure modes

  • Cache cluster down: the store must absorb full read load, so size it for that or degrade to serving cached-at-edge links only.
  • Key generation service down: servers keep working from their reserved blocks; alert long before blocks run out.
  • A region outage: replicate the link store across regions and route clients with DNS to the nearest healthy region.

Core algorithm in code

ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"


def encode(n: int) -> str:
    """Base62-encode a non-negative id."""
    if n == 0:
        return ALPHABET[0]
    out = []
    while n:
        n, r = divmod(n, 62)
        out.append(ALPHABET[r])
    return "".join(reversed(out))


class IdBlock:
    """Hands out ids from a reserved block, like one app server would."""
    def __init__(self, start: int, size: int):
        self.next, self.end = start, start + size

    def next_id(self) -> int:
        if self.next >= self.end:
            raise RuntimeError("block exhausted: reserve a new one")
        self.next += 1
        return self.next - 1


block = IdBlock(start=125_000_000, size=10_000)
for _ in range(3):
    i = block.next_id()
    print(i, "->", encode(i))
print("7-char capacity:", 62 ** 7)

Trade-offs

  • 302 keeps analytics and control but costs a request per click; 301 is cheaper but browsers cache it forever.
  • Counter-based codes are collision-free but sequential, so shuffle them if guessability matters.
  • A key-value store scales simply but makes queries like 'all links of a user' need a secondary index.

What a strong answer shows

  • Separates the read path from the write path and optimises reads.
  • Uses estimates to justify caching and the storage choice.
  • Generates codes without per-request coordination or collisions.
  • Keeps analytics off the critical path.
  • Names failure modes and how links keep working.

Mistakes to avoid

  • Designing the write path in depth while the interview is really about reads.
  • Using 301 redirects without noticing that analytics break.
  • Relying on hash truncation without a collision plan.
  • Updating click counters synchronously inside the redirect.

Follow-ups to expect

  • How would you support custom domains per customer? (Host header routing plus per-domain keyspace.)
  • How would you block malicious links? (Async scanning, then disable the code.)
  • How do you delete expired links at scale? (TTL in the store or a background sweep.)

Practice next

Next Post Previous Post
No Comment
Add Comment
comment url