System design guide
Design a URL shortener (TinyURL / Bitly)
The URL shortener is the most common warm-up question in system design rounds because it looks trivial and isn't. The interesting parts are generating short, unique codes at scale and serving a read-heavy workload with low latency. Here is a complete answer you can run in 45 minutes.
1. Clarify the requirements
Spend the first few minutes pinning down scope. A good set of questions: Do users need accounts? Can they pick custom aliases? Do links expire? Do we need click analytics? Are we building for a global audience?
Functional requirements
- Given a long URL, return a short URL such as
sho.rt/aZ3kP9q. - Visiting the short URL redirects to the original URL.
- Optional custom alias and optional expiry time.
- Basic click analytics (count, referrer, country) per link.
Non-functional requirements
- Low-latency redirects: p99 under roughly 50 ms at the edge. Redirects are the hot path.
- High availability for reads. A broken short link is a broken link on someone else's website.
- Codes are unique and not trivially guessable (sequential IDs leak volume and let people enumerate links).
- Analytics can be eventually consistent; a few seconds of lag is fine.
2. Back-of-the-envelope estimates
State assumptions, round aggressively, and use the numbers to drive decisions. (The method is covered in back-of-the-envelope estimation.)
| Quantity | Assumption | Result |
|---|---|---|
| Writes | 100M new links/day ÷ ~86,400 s | ~1,200/s average, ~3,500/s peak |
| Reads | 100:1 read-to-write ratio | ~120,000/s average, ~350,000/s peak |
| Storage | 100M/day × 365 × 5 years × ~500 bytes/row | ~180 billion rows, ~90 TB |
| Code space | 627 | ~3.5 trillion codes; we use ~5% in 5 years |
| Hot cache | ~20% of a day's links get most clicks | 20M × 500 B ≈ 10 GB, fits in memory |
The conclusions to say out loud: this is read-heavy, so the read path needs caching and should be separable from the write path; the dataset is too big for one machine, so we shard; the hot set is small, so a cache will absorb most traffic.
3. API
POST /v1/urls
{ "long_url": "https://...", "alias": "optional", "expires_at": "optional" }
→ 201 { "code": "aZ3kP9q", "short_url": "https://sho.rt/aZ3kP9q" }
GET /{code}
→ 302 Location: https://... (404 if unknown, 410 if expired)
GET /v1/urls/{code}/stats
→ { "clicks": 1234, "by_country": {...}, "by_day": [...] }
Authenticate the create endpoint with an API key and rate-limit it per key; the redirect endpoint is public. See design a rate limiter if the interviewer pushes on abuse.
4. High-level architecture
Write path. The Shorten API validates the URL (scheme, length, a malware/blocklist check), gets a new code, and writes code → long_url to the store. It returns the short URL.
Read path. The redirect service looks the code up in Redis; on a miss it reads the store and populates the cache. It returns a 302 and publishes a click event to Kafka without waiting. Splitting reads and writes into separate services lets you scale the redirect fleet independently, which matters at a 100:1 ratio.
5. Generating short codes
This is the core of the question. There are three standard approaches, and interviewers want to hear the trade-offs.
| Approach | How | Pros | Cons |
|---|---|---|---|
| Hash + truncate | MD5/SHA-256 of the URL, base62-encode, take 7 chars | Stateless; same URL gives same code | Collisions; you must check and retry, which needs a read before every write |
| Global counter + base62 | Increment a counter, encode it | No collisions, compact codes | Counter is a bottleneck and single point of failure; codes are sequential and guessable |
| Pre-allocated ID ranges | Each API server leases a block (e.g. 1M IDs) from a coordinator, then encodes locally | No collisions, no per-write coordination, scales horizontally | Gaps when a server dies mid-block (harmless); still sequential within a block unless you permute |
A strong answer picks range allocation: a small coordinator (a row in a strongly consistent store, or ZooKeeper/etcd) hands out ID blocks; servers encode IDs locally. To stop codes being guessable, run the integer through a reversible bijective scramble (for example a Feistel network or a multiply-by-coprime-mod-627) before base62 encoding. Custom aliases bypass the generator and use a conditional insert ("put if absent") so two users can't claim the same alias.
6. Data model
urls
code CHAR(7) PRIMARY KEY -- partition key
long_url TEXT NOT NULL
owner_id BIGINT NULL
created_at TIMESTAMP
expires_at TIMESTAMP NULL
clicks_daily -- written by analytics workers
code, day, country, count PRIMARY KEY (code, day, country)
Every lookup is by code, so shard by code using consistent hashing; with scrambled IDs, codes are uniformly distributed and shards stay balanced. If users need "list my links", add a secondary table keyed by owner_id rather than scatter-gather across shards. Raw click events stay in the log/warehouse; only aggregates live in the serving store.
7. Scaling the read path
- Cache aggressively. Mappings are immutable (unless you allow edits), so cache-aside with a long TTL and LRU eviction is safe. A 10–20 GB Redis cluster should absorb most redirects.
- Push popular redirects to the edge. A CDN or edge worker can serve the 302 for viral links with a short TTL, which also helps with sudden traffic spikes.
- Replicate the store across availability zones and serve reads from replicas. Read-your-write consistency rarely matters here, but if the creator clicks their link immediately, fall back to the primary on a miss.
- Negative caching. Cache "not found" briefly so scanners hammering random codes don't reach the database.
- Expiry. Check
expires_aton read and return 410; a background job deletes expired rows later. Don't rely on the cache to expire them for you.
8. Trade-offs to call out
- 301 vs 302: 301 reduces load but kills analytics and makes destination changes impossible; most products choose 302.
- Dedup the same long URL? Returning the existing code saves space but requires an index on
long_urland leaks that someone else shortened it. Per-user dedup is a reasonable middle ground. - Synchronous vs async analytics: writing counts inline adds latency and a failure mode to every redirect; a log plus stream processing keeps redirects fast at the cost of seconds of lag.
- Consistency: the mapping store can be AP (available, eventually consistent) because a mapping is written once; the ID allocator must be CP so blocks never overlap.
9. Common follow-up questions
- How do you prevent abuse (phishing, spam)? Rate-limit creation per key/IP, check URLs against a safe-browsing list at creation and periodically afterwards, and support takedowns that return a warning page.
- What happens if the ID allocator goes down? Servers already hold leased blocks, so writes continue for a while; run the allocator with a consensus-backed store so it fails over.
- How would you support multiple regions? Give each region its own ID range prefix so they never collide, replicate mappings asynchronously, and route users to the nearest region with GeoDNS.
- A celebrity tweets a link and it gets 1M clicks a minute. The edge cache and Redis absorb it; make sure the click pipeline is partitioned so one hot key doesn't overload a single consumer (add a random suffix to the partition key and aggregate later).
- How do you delete a user's data? Delete mappings by
owner_idvia the secondary index, purge caches, and let analytics aggregates age out according to policy.
If you want to practise this structure on other prompts, the system design interview framework lays out the same steps with timings.
FAQ
Should a URL shortener use SQL or NoSQL?
Either works. The access pattern is a single-key lookup with no joins, so a sharded key-value store (DynamoDB, Cassandra) is the natural fit at very large scale. A sharded relational database is fine and easier to reason about if you also need user accounts and per-user link lists. What matters is that you shard by short code and justify the choice.
301 or 302 redirect?
A 301 is cached by browsers, so repeat clicks never reach you: less load, but you lose click analytics and can't change the destination. A 302 (or 307) sends every click through your service. Most commercial shorteners use 302 because analytics is the product.
How long should the short code be?
Seven base62 characters give about 3.5 trillion codes, which covers 100 million new links a day for decades. Six characters (about 57 billion) is enough for smaller systems. Do the arithmetic out loud in the interview.