The interviewer slides a marker across the table. "Design bit.ly." It sounds like a ten-minute problem: hash the URL, store it, redirect. Then come the follow-ups. How do two servers avoid handing out the same code? What happens when one link goes viral? Why did you pick 302 and not 301? The question is small on purpose, so every decision shows.
Build a service like bit.ly or TinyURL. A user pastes a long URL and gets a short one, for example https://sho.rt/aZ3kL9q. Anyone who opens the short link is redirected to the original.
In scope
- Create a short link, optionally with a custom alias and an expiry
- Redirect, fast and highly available
- Click counts per link
At an assumed 100M new links a month and 100 redirects per link, a shortener handles about 40 creates and 4,000 redirects a second and stores 6 billion links (about 3 TB) over five years, using 0.17% of the 62^7 code space. Shorten servers lease blocks of 10,000 IDs from one counter row, scramble and base-62 encode them, and write with put-if-absent; redirects answer 302 from Redis cache-aside over a sharded URL store and publish clicks to Kafka. Failure modes: Redis down falls through to the store, an allocator outage is covered by leased blocks, and abuse and expiry are checked on create and read.
Phase 1: Requirements (3 minutes)#
"Is this mostly reads? Can people choose their own alias? Do links expire? Do we need analytics in real time?"
"Overwhelmingly reads. Custom aliases yes, optional. Default links live five years. Click counts can lag by a minute."
Functional requirements#
shorten(long_url, alias?, expires_at?)returns a unique short codeGET /{code}redirects to the long URL, or returns 404 if the code is unknown or expired- Owners can delete a link
- Click counts per link, eventually consistent
Non-functional requirements#
- Read heavy: about 100 redirects per creation
- Low latency: redirect under 50 ms at p99 on the server
- Highly available: a broken redirect breaks every page and message that embedded it, so redirects get the strictest availability target
- Unique codes: two different links must never get the same code
- Not guessable: codes should not be easy to enumerate one after another
Phase 2: Estimation (3 minutes)#
Before you calculate: 6 characters or 7?
Over five years at 100M links a month you store about 6 billion links. With 62 symbols (a-z, A-Z, 0-9), is a 6-character code enough?
One strong answer
62^6 is about 56.8 billion, so 6 billion links would use about 11% of the space. A counter-based scheme could live with that, but random codes would collide about one time in ten, and a dense space is easy to enumerate. 62^7 is about 3.5 trillion: the same 6 billion links use about 0.17%, so 7 characters buys headroom for one extra character of length.
- Writes: 100M per month ÷ ~2.6M seconds per month ≈ ~40 creates/s (assume 3x peaks: ~120/s)
- Reads: 100 × 40 ≈ ~4,000 redirects/s average, ~12,000/s at a 3x peak
- One record (code, long URL, owner, created, expires): assume ~500 bytes with index overhead
- Five years: 100M × 60 months = 6 billion links × 500 B ≈ 3 TB, about 9 TB with 3 replicas
- Cache: if the hottest 20M links take most of the clicks, 20M × 500 B ≈ 10 GB, which fits in one Redis node and its replicas
"Write traffic is tiny. The design is a read-path problem: keep redirects in memory and keep the write path correct."
Phase 3: API (2 minutes)#
POST /api/v1/urls
Authorization: Bearer <api key>
{ "long_url": "https://example.com/very/long/path?utm=x", "alias": "spring-sale", "expires_at": "2031-01-01" }
201 Created
{ "code": "aZ3kL9q", "short_url": "https://sho.rt/aZ3kL9q" }
GET /aZ3kL9q
302 Found
Location: https://example.com/very/long/path?utm=x
DELETE /api/v1/urls/aZ3kL9q → 204 (owner only)- An alias that is already taken returns 409 Conflict.
- Unknown and expired codes return 404, and the 404 is cached too (more on that below).
Phase 4: High-level design (4 minutes)#
"Two paths with very different needs: a small write path that must never duplicate a code, and a huge read path that must never be slow."
%% title: URL shortener: create path and redirect path
flowchart TD
U@{ icon: "sd:users", label: "Users, browsers, apps" } --> LB@{ icon: "sd:load-balancer", label: "Load balancer" }
LB -->|"POST /api/v1/urls"| WS["Shorten service<br/>holds a block of IDs"]:::core
LB -->|"GET /{code}"| RS["Redirect service"]:::hot
WS -->|"next block of 10,000"| KA@{ icon: "sd:key", label: "Range allocator<br/>(one counter row)" }
WS -->|"put-if-absent"| DB@{ icon: "sd:database", label: "URL store<br/>key-value, sharded by code" }
RS -->|"1 · cache-aside"| R@{ icon: "tech:redis", label: "Redis<br/>hot codes + 404s" }
RS -->|"2 · on miss"| DB
RS -->|"click event, async"| K@{ icon: "tech:kafka", label: "Kafka · clicks" }
K --> AN@{ icon: "sd:chart", label: "Click counts" }
class U client
class LB edge
class KA,DB store
class R cache
class K queue
class AN service- Shorten service: validates the URL, takes the next ID from its local block, turns it into a 7-character code and writes
code → long_urlwith put-if-absent. - Redirect service: stateless. Looks up the code in Redis, falls back to the URL store, answers with a 302, and publishes a click event without waiting for it.
- URL store: a key-value lookup by code, so any store that shards by key works (a key-value database, or sharded SQL keyed on
code). - Click counts: a consumer aggregates Kafka events into per-link counters; they lag by seconds to a minute, as agreed.
Phase 5: Deep dives (5 minutes)#
Deep dive 1: Where do codes come from?#
"Two shorten servers get a request at the same millisecond. How do you make sure they never produce the same code?"
Hash and truncate
base62(md5(long_url))[:7]- Same URL always gives the same code (handy for dedupe)
- Truncation collides: you must check and re-hash, and two servers can race on the check
Random + check
- Pick 7 random characters, insert if absent
- Simple, unguessable, no coordination
- Every insert needs a uniqueness check; retries grow as the space fills
Counter + base62
- A unique integer per link, encoded in base 62
- Never collides by construction
- Needs a counter that many servers can share without a bottleneck
"I'd use the counter, but without a round trip per link. Each shorten server leases a block of 10,000 IDs from a range allocator, a single row it increments atomically. At 40 creates a second the allocator is touched a few times an hour, and a server that crashes just wastes the rest of its block."
- Lease a block: the server atomically moves the allocator's counter from N to N + 10,000 and owns that range.
- Take the next ID from local memory: no network call.
- Scramble it: a reversible permutation of the 62^7 space (for example multiply by a large number coprime to 62^7, modulo 62^7) so consecutive IDs don't give consecutive codes.
- Encode the result in base 62, left-padded to 7 characters.
- Write with put-if-absent: a custom alias goes through the same conditional write, which is what returns 409 when the alias is taken.
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
SPACE = 62 ** 7 # ~3.5 trillion codes
MULT = 1_580_030_173 # any odd number not divisible by 31 is coprime to 62^7
def to_code(n: int) -> str:
n = (n * MULT) % SPACE # bijective scramble: unique in, unique out
out = []
for _ in range(7):
n, r = divmod(n, 62)
out.append(ALPHABET[r])
return "".join(reversed(out))The scramble only hides the order from casual eyes; it is not encryption. If enumeration is a real threat (private documents behind short links), use random codes with put-if-absent, or longer codes.
Deep dive 2: Making the redirect fast and cheap#
"Why 302? A 301 is cacheable by browsers, so you'd get less traffic."
301 Moved Permanently
- Browsers may cache it and skip your server next time
- Less load, but you stop seeing repeat clicks
- Hard to take back: an edited or deleted link can keep redirecting from caches
302 Found
- Every click comes back to you
- Accurate click counts, and delete or expiry works at once
- More traffic, which the cache is there to absorb
"Analytics and the ability to delete are requirements, so 302. Then I make the extra traffic cheap."
- Cache-aside in Redis: read the code from Redis; on a miss, read the store and set it with a TTL. With a skewed click distribution, most redirects never touch the store.
- Cache the misses too: scanners try random codes. Storing "not found" for a short TTL stops them from hammering the store.
- Viral links: one code can get a burst far above its neighbours. Short-lived caching at a CDN edge, or a small in-process cache in each redirect server, absorbs it before it reaches a single Redis shard.
- Clicks off the critical path: the redirect publishes a click event to Kafka and returns. If Kafka is slow, the redirect still works; counts catch up later.
- Expiry on read: the cached value carries
expires_at, and the redirect service checks it before answering, so an expired link stops at once even if a cache entry outlives it.
Codes and redirects
Q1/2 Why does each shorten server lease a block of 10,000 IDs instead of asking the allocator for every link?
Answer & explanation
Answer: B
The allocator is touched a few times an hour instead of on every create. A crash only wastes the unused part of a block. Guessability is handled by the scramble step, not by blocks.
Q2/2 The product team wants accurate click counts and the ability to kill a link instantly. Which redirect status fits?
Answer & explanation
Answer: B
A 301 can be cached by browsers, so repeat clicks bypass you and a deleted link may keep working. 302 keeps you in the loop; the cache absorbs the cost.
Phase 6: Trade-offs and failure modes (2 minutes)#
Redis is down
Redirects fall through to the URL store. Size the store's read replicas for the peak (about 12,000 reads/s here) so a cache outage is slower, not fatal.
The range allocator is down
Servers keep issuing IDs from the block they already hold. At 40 creates a second across the fleet, a block lasts a long time, so creation survives a short outage untouched.
The same long URL is shortened twice
Simplest answer: give each request its own code (owners, expiry and stats differ). Deduplicate only per owner if the product asks for it, with a lookup index on (owner, long_url).
Abuse
Check new URLs against a malware and phishing list at creation, rate-limit creation per API key, and keep a blocklist the redirect service consults so a bad link can be killed in seconds.
Expired links pile up
Check expiry on read (correctness) and run a background sweeper that deletes expired rows in small batches (storage). The sweeper can lag without anyone noticing.
Phase 7: Go deeper in the course#
- The full version of this problem, with sharding, hot keys and rate limits at 100M links a day: Week 1 Capstone: URL Shortener at Scale
- Sharding the URL store by code, consistent hashing: Week 1 · Day 1: Partitioning Deep-Dive
- Viral links and hot keys: Week 1 · Day 4: Hot Keys and Skew
- Cache-aside, TTLs and negative caching: Week 4 · Day 1: Caching Patterns
- Edge, in-process and Redis caches together: Week 4 · Day 5: Multi-Tier Caching
- Rate-limiting the create API: Week 1 · Day 3: Rate Limiting at Scale
- Base 62 and capacity math: Week 0 · Part 3: Back-of-the-Envelope Estimation
Final check#
Final check · URL shortener
Q1/3 At 100M new links a month and 100 redirects per link, roughly what average redirect rate do you design around?
Answer & explanation
Answer: B
100M per month is about 40 creates per second (100M ÷ ~2.6M seconds). A 100:1 ratio gives about 4,000 redirects per second, and about 12,000 at a 3x peak.
Q2/3 Why is a 6-character code a weak choice for about 6 billion links?
Answer & explanation
Answer: A
62^6 is about 56.8 billion, so the links would fit, but densely. 62^7 (about 3.5 trillion) drops the fill to about 0.17%.
Q3/3 A scanner hits millions of random codes that don't exist. What protects the URL store?
Answer & explanation
Answer: C
Negative caching turns repeated misses into cache hits, and a per-client limit stops one caller from sweeping the space.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.