System design interview: design a URL shortener
A URL shortener maps a long address to a short code and redirects anyone who opens the code. The interview is about three decisions: how you generate codes that never collide, how you serve a read-heavy redirect path in a few milliseconds, and what you give up by choosing a 301 or a 302 redirect.
By Evin JohnPublished 7 min read
1. Clarify the requirements
Spend the first few minutes agreeing on scope. Interviewers often leave custom aliases, expiry and analytics ambiguous on purpose to see whether you ask.
- Functional: create a short link for a long URL; redirect a short link to its target; optional custom alias; optional expiry date; click counts per link.
- Non-functional: redirects must be fast (aim for under 50 ms at the server) and highly available, because a broken short link breaks someone else's page, email or QR code. Creating links can be slightly slower.
- Out of scope unless asked: user accounts and billing, link editing after creation, a full analytics dashboard.
2. Estimate the scale
Pick round numbers, state them as assumptions and use them to justify choices later. The figures below are illustrative, not any real company's traffic.
| Quantity | Assumption | Result |
|---|---|---|
| New links | 100 million a day | about 1,160 writes a second on average |
| Redirects | 10 reads for every write | about 11,600 reads a second; plan for 3x at peak, about 35,000 |
| Records over 5 years | 100 million x 365 x 5 | about 183 billion mappings |
| Storage | about 500 bytes a record (code, URL, owner, dates) | about 90 TB before replication |
| Code length | base62 (a–z, A–Z, 0–9), 7 characters | 62^7 ≈ 3.5 trillion codes, enough for decades at this rate |
Two conclusions fall out of the numbers: the data set is too large for one machine, so it has to be partitioned; and the read rate is easy for a cache, because a small share of links (a viral post, a campaign) takes most of the clicks.
3. Define the API
POST /api/v1/links
body: { "url": "https://example.com/a/very/long/path", "alias": "spring-sale", "expiresAt": "2027-01-01" }
returns: 201 { "code": "spring-sale", "shortUrl": "https://sho.rt/spring-sale" }
GET /{code}
returns: 302 Location: https://example.com/a/very/long/path
404 if the code does not exist, 410 if it has expired4. Sketch the data model
| Field | Type | Why it is there |
|---|---|---|
code | string, primary key | The lookup key for every redirect; also the partition key |
long_url | string (up to ~2 KB) | The target |
created_at / expires_at | timestamps | Expiry checks on read and a cleanup job |
owner_id | string, nullable | Who may delete it; rate limits per owner |
Click counts do not belong in this row. Incrementing a counter on every redirect turns the hottest read path into a write path. Send click events to a queue instead (section 7).
5. Generate codes that never collide
This is the decision the interviewer most wants to hear you reason about. There are three common answers, and each has a cost.
| Approach | How it works | Strength | Cost |
|---|---|---|---|
| Hash and truncate | MD5 or SHA-256 of the URL, keep the first 7 base62 characters | Same URL always gets the same code; no coordination | Collisions are certain at scale, so every write needs a check-and-retry; two users shortening the same URL share a link |
| Global counter + base62 | An auto-incrementing ID, encoded in base62 | No collisions, short codes, simple | The counter is a single point of contention; sequential codes are guessable, so links can be enumerated |
| Pre-allocated key ranges | A key service hands each app server a block of IDs (say 10,000) to encode locally | No collisions and no per-write coordination; scales out | A crashed server wastes the rest of its block (harmless with 3.5 trillion codes); one more service to run |
A strong default is the range approach: a small coordination service (or a database sequence stepped by 10,000) hands out ranges, and each app server encodes its own IDs. If guessable codes matter, pass the ID through a reversible shuffle (a keyed permutation of the 62^7 space) before encoding, so neighboring IDs give unrelated codes.
Custom aliases use the same table: insert with a conditional write ("only if this code does not exist") and return 409 Conflict if it is taken. Reserve words like api or admin so an alias can never shadow a route.
6. The architecture
- Edge
- CDN (optional, caches 301s)
- Load balancer
- App tier
- Stateless app servers
- Rate limiter
- ID range allocator
- Hot path
- Redis cache (code → URL, LRU)
- Storage
- Key-value store, partitioned by code
- Replicas in 2+ zones
- Async
- Click event queue
- Analytics aggregator
- Expiry cleanup job
- RedirectLook up the code in Redis. On a miss, read the store, write the result back to the cache, then answer 302 with the target. Check
expires_atbefore answering. - CreateValidate the URL (scheme, length, a blocklist of known malicious domains), take the next ID from the server's range, encode it, write the row, return the short URL.
- CountPublish a small event (code, timestamp, referrer, country) to the queue after the redirect is sent, so analytics never slows the response.
7. Scale it
- Cache the hot set. With an 80/20 access pattern, caching the most-clicked 20% of a day's links serves most reads from memory. A few hundred GB of Redis, sharded, is plenty. Use LRU eviction.
- Partition the store by code. A hash of the code spreads load evenly; range partitioning by code would put all new (sequential) links on one partition. DynamoDB, Cassandra or a sharded relational database all work.
- Keep app servers stateless so you add them behind the load balancer as traffic grows. The only state they hold is their unused ID range.
- Replicate across zones for availability. Redirects can read from a replica: a link created a second ago being briefly unavailable on one replica is acceptable.
- Move analytics off the hot path with the queue. Aggregate into per-minute counts per code in a column store or time-series database.
8. What breaks, and what you do about it
| Failure | Effect | Mitigation |
|---|---|---|
| Cache cluster down | Every redirect hits the store; latency and load spike | Size the store for cache-miss traffic at normal load; circuit-break and shed analytics first |
| A link goes viral | One key gets millions of reads a minute (a hot key) | Local in-process cache on app servers for the top keys; CDN caching for that code |
| ID allocator unavailable | New links cannot be created | Servers keep a spare range so creates continue for minutes; redirects are unaffected |
| Abuse: phishing links | Your domain gets blocklisted by browsers and mail filters | Scan targets on create and periodically, rate-limit anonymous creates, add a takedown path |
9. Trade-offs to discuss
- 301 or 302. A 301 (permanent) is cached by browsers, which cuts your read traffic but hides repeat clicks and makes a changed or deleted link stick in caches. A 302 (or 307) is counted every time. Most shorteners that sell analytics use 302.
- SQL or NoSQL. The access pattern is a single-key lookup with no joins, which suits a key-value store. A relational database is simpler to run and is a fine answer for a smaller scope; say where you would switch.
- Guessable or random codes. Sequential codes are shorter and cheaper; random-looking codes protect private links from enumeration. You can have both with a keyed shuffle.
- Deduplicate or not. Returning the same code for the same URL saves space but leaks that someone else shortened it, and breaks per-user analytics. Most designs create a new code per request.
10. How to talk through it in the interview
Fictional example
- Candidate
- Before I draw anything: do we need custom aliases, expiry and click analytics, or just create and redirect?
- Interviewer
- Aliases and basic click counts, yes. Expiry is optional.
- Candidate
- Then I will assume about 100 million new links a day and ten redirects per create. That is roughly 12,000 reads a second, so the redirect path is what I will optimize. I will cover code generation, the read path with a cache, storage partitioning, and then analytics off the hot path.
- Name the read/write ratio early and keep coming back to it.
- When you pick a code-generation scheme, name the one you rejected and why.
- Leave time for failure modes. "What happens when the cache dies?" is the most common follow-up.
Practicing this design with Natively
Run the walk-through as a mock interview: ask a friend to play the interviewer on a call, or read the prompt aloud yourself. Natively transcribes the conversation as it happens and, when a question comes in, can suggest talking points for it. Comparing those with what you actually said is a quick way to spot gaps such as failure modes or the redirect status code. After the session, the transcript and notes stay on your computer, so you can see where you ran long and what you skipped.
Questions people ask
How many characters should a short code have?
Seven base62 characters give about 3.5 trillion combinations, which covers 100 million new links a day for more than 90 years. Six characters (about 57 billion) is enough for a smaller service. Say the arithmetic out loud in the interview; the number itself matters less than showing you checked it.
Should a URL shortener use a hash of the URL?
Hashing gives the same code for the same URL without coordination, but a truncated hash collides at scale, so every write needs a check and a retry. A counter or pre-allocated ID ranges encoded in base62 avoid collisions entirely, which is why most interview answers prefer them.
Is a 301 or a 302 redirect better for a URL shortener?
A 301 is permanent, so browsers cache it and later clicks never reach your servers: less load, but no click counts and no way to change the target. A 302 (or 307) is temporary, so every click is seen. Services that report analytics generally use 302.
Where to go next
Practice with the same assistant you would use on a call: it listens, transcribes and suggests answers from your own resume and the job description.
Facts on this page were last checked on . Product details come from the Natively app itself; if something here is out of date, tell us at hello@heynatively.com.
