System design crash course

Deep dive · Warm-up

Design a url shortener

The canonical warm-up. Small surface area, but it exposes whether you can reason about ID generation, collisions, extreme read skew, and cache strategy without hiding behind buzzwords.

What it trains: Hashing, ID generation, read-heavy caching, simple sharding

1. Requirements

Functional

  • Create a short code for a long URL, optionally with a custom alias and an expiry
  • Redirect a short code to the original URL with an HTTP 301/302
  • Report basic click analytics per link

Non-functional

  • Extremely read-heavy: roughly 100:1 reads to writes
  • Redirect latency under ~50ms at p99 — the redirect is on the user's critical path
  • High availability for reads; a brief write outage is survivable
  • Short codes are permanent and must never be reassigned

2. Back-of-envelope numbers

  • 100M new links/day ≈ 1,200 writes/sec; 10B redirects/day ≈ 120K reads/sec
  • ~500 bytes per record × 100M/day ≈ 50 GB/day, ~18 TB/year — a sharded KV store, not one Postgres box
  • Base62 with 7 characters gives 62^7 ≈ 3.5 trillion codes — enough for decades
  • The hot 20% of links serve ~80% of traffic, so a cache of a few hundred GB absorbs nearly all reads

3. Architecture

        ┌────────┐
        │ Client │
        └───┬────┘
            │  GET /abc1234
      ┌─────▼──────┐
      │    CDN     │  (edge cache for hot codes)
      └─────┬──────┘
      ┌─────▼──────┐
      │    Load    │
      │  Balancer  │
      └─────┬──────┘
   ┌────────┴────────┐
   │  App servers    │──── ID range lease ───▶ ┌──────────────┐
   │  (stateless)    │                         │ ID generator │
   └───┬─────────┬───┘                         │  (ZooKeeper/ │
       │         │                             │   counter)   │
  ┌────▼───┐ ┌───▼─────────┐                   └──────────────┘
  │ Redis  │ │  KV store   │
  │ cache  │ │ (Dynamo /   │
  └────────┘ │  Cassandra) │
             └───┬─────────┘
                 │ click events
            ┌────▼────┐    ┌──────────────┐
            │  Kafka  │───▶│ Analytics DB │
            └─────────┘    └──────────────┘

Reads short-circuit at the CDN and cache; writes take a leased ID range so no two servers can mint the same code.

API contract

Two endpoints do the work. Keep them boringly simple — the complexity lives underneath.

  • POST /links { longUrl, customAlias?, expiresAt? } → { shortUrl }
  • GET /{code} → 302 redirect to the long URL
  • GET /links/{code}/stats → click counts by day, referrer, country

Data model

A single wide row keyed by short code is all you need: code (partition key), long_url, owner_id, created_at, expires_at. Click counts live in a separate analytics store — never increment a counter in the redirect path.

Redirect path

Look up code → cache hit returns immediately; miss reads the KV store and populates the cache. Emit a click event asynchronously to Kafka so analytics never adds latency or a failure mode to the redirect.

4. Deep dives

This is where levels are decided. Go deep on one or two of these rather than shallow on all of them.

Generating the short code

There are three defensible approaches, and the interviewer wants you to compare them rather than pick one silently.

  • Hash the URL (MD5/SHA) and take the first 7 base62 characters — simple, but collisions require a check-and-retry on every write.
  • Global counter encoded in base62 — no collisions, but the counter is a single point of contention and codes are guessable/enumerable.
  • Counter with leased ranges (best): each app server leases a block of 10,000 IDs from ZooKeeper or a Postgres sequence and hands them out locally. One coordination call per 10,000 writes, no collisions, no hot row.

Making reads fast

Redirects are the product. Layer three caches: the CDN/edge caches redirects for popular codes with a TTL; Redis holds the hot working set; the KV store is the source of truth. Use a 301 only when links are truly immutable — browsers cache 301s aggressively and you lose per-click analytics. A 302 keeps control but costs a round trip every time.

Sharding and hot keys

Partition by short code — it is a uniformly random key, so partitions stay balanced without any resharding cleverness. The real risk is a single viral link creating a hot partition; the cache and CDN absorb that, which is exactly why they exist rather than being decoration.

Custom aliases and expiry

Custom aliases skip the generator and go through a conditional write (insert-if-not-exists) so two users can't claim the same alias. Expiry is a TTL column plus a background sweeper — do not rely on TTL alone if you must recycle or audit codes; simply refusing to reuse codes avoids an entire class of security bugs where an old link points at a stranger's destination.

Analytics without slowing the redirect

Fire-and-forget a click event (code, timestamp, referrer, geo, user agent) to Kafka. A stream job aggregates into per-day counters in a columnar store. This gives you approximate-real-time stats and lets analytics fail without taking redirects down.

5. Tradeoffs to argue

301 vs 302 redirect

Upside: 301 is cached by the browser: near-zero server load for repeat visits

Cost: You lose per-click analytics and can never change the destination

Hashing vs counter

Upside: Hashing is stateless and trivially parallel

Cost: Collision checks add a read on every write; counters need coordination but are exact

SQL vs KV store

Upside: Postgres is simpler and gives you transactions for alias uniqueness

Cost: At 10B reads/day and 18 TB/year, a KV store's horizontal scale wins

6. Questions the interviewer will ask

  • What happens when two users request the same custom alias at the same moment?
  • Your Redis cluster restarts cold. What does the KV store see, and does it survive?
  • How would you prevent someone from enumerating every link you've ever created?
  • How do you delete a link for GDPR when it's cached at 200 CDN edges?

7. Mistakes that sink this answer

  • Incrementing a click counter synchronously in the redirect path
  • Choosing a global counter without mentioning the contention it creates
  • Skipping the collision-handling story after choosing hashing
  • Designing a relational schema with joins for something with one access pattern

Practice this live

Run a mock system design interview and get scored against your target level.

Start mock interview

Other problems