Design a URL Shortener: System Design Interview Walkthrough
A structured answer to the URL shortener (TinyURL) question: requirements, capacity estimates, key generation strategies, database choice, caching, redirects and the follow-ups that separate strong candidates.
URL shortening looks trivial and tests whether you can reason about scale, ID generation and read-heavy design. Pair this with the system design cheat sheet.
1. Requirements
Functional: create a short URL, redirect to the long URL, optional custom alias, optional expiry, optional click analytics. Non-functional: very low redirect latency, high availability, short codes unguessable enough, scalable.
2. Estimates
Assume 100M new URLs per month and 100:1 read:write.
- Writes: about 40 per second
- Reads: about 4,000 per second on average, with higher peaks
- Storage: 100M × 500 bytes ≈ 50 GB/month
The numbers tell you: a cache plus a partitioned store is plenty.
3. API
POST /api/v1/urls { longUrl, alias?, expiresAt? } -> { shortUrl }
GET /{code} -> 301/302 redirect to longUrl
4. Generating the short code
| Approach | Idea | Trade-off |
|---|---|---|
| Counter + base62 | Unique ID from a counter, encoded in base62 | No collisions; needs a distributed counter (ranges handed to app servers) |
| Hash + truncate | MD5/SHA of the URL, take 7 chars | Stateless, but collisions need a retry or check |
| Key generation service | Pre-generate random keys, hand them out | Fast writes; extra component |
| Random + check | Random code, insert if absent | Simple; retry loop at scale |
A common answer: counter ranges per server, base62-encode them, and add a random salt if guessability matters.
5. Data model and storage
code (PK) -> longUrl, userId, createdAt, expiresAt. The access pattern is single-key lookup, so partition by code. Add a TTL for expiry.
6. Reads: cache first
- Cache
code -> longUrlin Redis with an LRU policy; the hot set is small. - On miss, read the database and populate the cache.
- Handle negative caching for unknown codes to avoid hammering the DB.
7. Scaling and reliability
- Stateless app servers behind a load balancer
- Database sharding by code, with replicas for read availability
- Rate limit creation to prevent abuse (see rate limiter design)
- Detect malicious URLs, and validate input
8. Follow-ups
- How do you support custom aliases without collisions?
- How do you count clicks without slowing redirects? (async events to a queue)
- How do you expire and reclaim codes?
- What if the counter service fails? (pre-allocated ranges)
Worked example: base62 encoding
To turn a numeric ID into a short code, repeatedly divide by 62 and map remainders to the alphabet of digits, lower-case and upper-case letters. Seven characters give 62 to the power of 7, about 3.5 trillion codes. If a counter hands out 100 million IDs a month, seven characters last for thousands of years.
If you worry about guessability (sequential codes let someone enumerate all URLs), shuffle the ID with a reversible permutation or add a random component before encoding.
Distributed ID generation
| Option | How | Trade-off |
|---|---|---|
| Single counter | One database sequence | Simple but a bottleneck |
| Range allocation | Each app server leases a block of 1,000 IDs | Few round trips; gaps after crashes |
| Snowflake-style IDs | Timestamp, machine ID, sequence | No coordination; longer codes |
| Key generation service | Pre-computed unique keys in a store | Fast; extra service to run |
Range allocation is a good default answer.
Handling custom aliases
Check availability with an insert-if-absent operation (a conditional write) rather than read-then-write, to avoid races. Reserve a blocklist of words such as admin or api routes.
Analytics without slowing redirects
Redirects should return immediately. Emit a click event (code, time, country, user agent) to a queue and process it asynchronously into aggregated counters. This is also where a 302 is needed, since a cached 301 would skip your server.
Security and abuse
- Validate and normalize submitted URLs; reject unsupported schemes
- Scan against known-malicious URL lists
- Rate limit creation per user and IP (rate limiter design)
- Prevent open-redirect abuse by showing an interstitial for flagged links
- Allow owners to disable a code
Expiry and cleanup
Store an expiry time and either rely on a database TTL or run a background job that deletes expired rows and frees the cache. Decide whether expired codes may be reused; usually they are not, to prevent old links from pointing somewhere new.
Interview tips
Spend your first five minutes on requirements and estimates, because the read-heavy ratio drives every later choice. Finish by naming the single points of failure (ID generator, database) and how you remove them.
Related reading
Frequently asked questions
Was this article useful?
Bring backup into your next round.
Install InterviewPilot, upload your resume, and walk in with structured answers a keystroke away — visible to you and nobody else.
More from the blog
Design a Chat Application: System Design Interview Walkthrough
How to answer the WhatsApp or Messenger system design question: WebSockets, message delivery and ordering, storage, presence, group chats, offline delivery and scaling to millions of connections.
Design a News Feed: System Design Interview Walkthrough
How to answer the news feed (Facebook, Instagram, Twitter timeline) question: fan-out on write vs read, the celebrity problem, feed ranking, storage, caching and the trade-offs interviewers want named.
Design a Notification System: System Design Interview Walkthrough
How to design a scalable notification system for push, SMS and email: architecture, queues per channel, retries and idempotency, user preferences, rate limiting and delivery tracking.
Discussion
No comments yet. Been through an interview like this? Your experience could help the next candidate.