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.
Other problems
Chat / messaging
WebSockets, ordering, delivery guarantees, presence
News feed
Fan-out on write vs read, ranking, the celebrity problem
Ride sharing
Geospatial indexing, matching, real-time state
Rate limiter
Token bucket, sliding window, distributed counters
Web crawler
Queues, deduplication, politeness, horizontal scale