Design a URL shortener like bit.ly: create short links, redirect quickly at scale, and track click counts.
Key points
- Clarifies scale (reads dominate writes 100:1), link lifetime and whether custom aliases are neededIn plain words: Ask first how busy it is, how long links live and whether people choose their own names, noting far more people click links than create them
- Chooses a 7-character base62 code generated from a unique id or random with collision checkIn plain words: Use a seven-character code of letters and digits, made from a unique number or picked at random and checked for clashes
- Serves redirects from a cache in front of a key-value or relational store, with 301 vs 302 decided by analytics needsIn plain words: Answer clicks from fast memory in front of a simple store, and pick a permanent or temporary redirect based on whether you need to count clicks
- Uses a counter service or async event stream for click analytics rather than a synchronous write on every redirectIn plain words: Count clicks on the side through a stream or counter, so the redirect never waits on a write
- Discusses hot links, expiry and abuse (malicious URLs, enumeration of sequential ids)In plain words: Talk about links that go viral, links that expire, and misuse such as harmful targets or guessing codes that go up in order
Stages
1Requirements
- Functional, shorten a URL, redirect, optional custom alias, optional expiry, click analytics
- Non-functional, redirect latency in the tens of milliseconds, very high availability, codes must not be guessable
2Estimates
- ~100M new links per month → ~40 writes/s, 100:1 reads → ~4,000 reads/s, peak 10x
- ~500 bytes per link → ~6 TB over 10 years
3API
- POST /links with longUrl, optional alias and expiry, returns shortUrl
- GET /{code} returns 301 or 302 to the long URL, 404 if unknown or expired
4Data model
- links(code PK, long_url, user_id, created_at, expires_at)
- clicks aggregated per code per day, not one row per click
5High-level design
- Stateless API servers behind a load balancer, cache (Redis) for code → URL, key-value or relational store behind it
- Separate id-generation or code-allocation service
6Deep dives
- Code generation, base62 of a unique id vs random with retry vs pre-generated key pool
- 301 (cacheable, fewer hits, less analytics) vs 302 (every click hits you, full analytics)
- Analytics via async events to a stream and periodic aggregation
7Bottlenecks and failure
- Hot link storms handled by cache and CDN
- Sequential ids leak creation volume, mitigate with random offset or encryption
- Malware scanning and rate limiting on creation
Model answer
Requirements
Functional: create a short link for a long URL, redirect when the short link is visited, optional custom alias, optional expiry, click counts per link. Non-functional: redirects must be fast (they sit in front of every page view the link drives) and highly available; codes should not be guessable; creation can be slightly slower.
Estimates
100 million links per month is ~40 writes/s; a 100:1 ratio gives ~4,000 reads/s, peak ~40,000. Ten years of links at ~500 bytes is ~6 TB. Small. The interesting constraints are latency and availability, not throughput.
API
POST /api/links { longUrl, alias?, expiresAt? } -> { code, shortUrl }
GET /{code} -> 302 Location: longUrl (404 if unknown/expired)
GET /api/links/{code}/stats -> { clicks, byDay[] }
Data model
links(code PK, long_url, owner_id, created_at, expires_at) and link_clicks_daily(code, day, count). The code is the primary key because every redirect is a point lookup by code.
High-level design
Redirect servers are stateless; the cache holds code → long_url for hot links; the store is a key-value database or a relational database with the code as primary key. Click events are published asynchronously so the redirect path never waits on analytics.
Deep dives
Code generation. Three common schemes:
- Base62 of a unique 64-bit id from a Snowflake-style generator: no collisions, but sequential codes let anyone enumerate your links and estimate volume. Mitigate by encrypting or bit-shuffling the id first.
- Random 7-character code with a uniqueness check: 62^7 ≈ 3.5 trillion codes, so collisions are rare; retry on conflict.
- Pre-generated key pool: a service fills a table with unused random codes; API servers lease batches. Fast and collision-free at the cost of a component to run.
301 vs 302. A 301 (permanent) lets browsers and CDNs cache the redirect, so repeat visits never reach you: lower load, but you lose click counts. A 302 (temporary) sends every click through you, giving full analytics. Most commercial shorteners use 302 for that reason.
Analytics. Never increment a counter row synchronously on each redirect; it serialises writes on hot links. Emit an event (code, timestamp, referrer, user agent) to a stream and aggregate per minute or per day.
Bottlenecks and failure modes
- A viral link produces millions of redirects per hour: the cache absorbs it, and a CDN can cache 302s briefly too.
- If the cache tier dies, the database must survive the full read load for a few minutes: size it for that or fail some requests gracefully.
- Abuse: scan destination URLs against phishing lists, rate-limit creation per user, and block known bad domains.
- Expiry: lazy-delete on read plus a background sweeper.
Likely follow-ups
- How would you support custom aliases without breaking uniqueness guarantees?
- What changes if links must be deleted or edited after creation?