Short answer: a URL shortener is a very large key-to-value lookup that is read about a hundred times for every time it is written. Generate a short, unique key on write (a counter turned into base62 works well), store the pair in a key-value or sharded SQL store, put a cache in front of the read path, and answer every redirect from the cache first. Everything else (analytics, expiry, abuse control) is built around that core.
This is a worked system design walkthrough, written the way I would explain it on a whiteboard. All numbers below are assumptions I chose to show the method, not measurements from a real service. In an interview, say your assumptions out loud and the interviewer can change them.
Step 1: Clarify what you are building
Start by asking questions, because “design a URL shortener” hides many decisions.
Functional requirements
- Given a long URL, return a short URL (for example
sho.rt/aZ3kP9x). - Opening the short URL redirects the visitor to the original long URL.
- Optional: a custom alias, an expiry date, and click counts.
Non-functional requirements
- Very fast redirects. Users feel a slow redirect on every click.
- High availability. A dead redirect service breaks every link anyone ever shared.
- Short links must not collide. Two long URLs must never get the same key.
- Keys should be hard to guess if links are meant to be private-ish.
Step 2: Estimate the scale
Assume 100 million new short URLs per month and 100 reads for every write.
| Quantity | Working | Result |
|---|---|---|
| Writes per second | 100,000,000 ÷ (30 × 86,400 s) | about 39 per second |
| Reads per second | 39 × 100 | about 3,900 per second (plan for 3× at peak, about 12,000) |
| Records after 5 years | 100M × 60 months | 6 billion |
| Storage after 5 years | 6B × about 500 bytes per record | about 3 TB |
| Cache for the hot 20% of one day | 3,900 × 86,400 × 20% × 500 B | about 33 GB |
What the numbers tell you: the write rate is tiny, so writes are not the hard part. Reads are modest too, but they are latency-sensitive. And about 33 GB of hot data fits in the memory of a small cache cluster, so most redirects can be served without touching the database.
Step 3: Choose the key length
A key made of the 62 characters a-z A-Z 0-9 (base62) gives:
- 6 characters: 626 = about 56.8 billion keys
- 7 characters: 627 = about 3.5 trillion keys
We need 6 billion keys over 5 years, so 6 characters would be enough. Seven gives huge headroom and keeps the link short, so 7 is a comfortable choice.
Step 4: Generate the key (the main design decision)
| Approach | How it works | Good | Watch out for |
|---|---|---|---|
| Hash the long URL | MD5 or SHA, take the first 7 base62 characters | Same long URL gives the same key; no counter needed | Collisions: two URLs can share a prefix, so you must check the database and retry |
| Counter + base62 | A global increasing number, written in base62 | Never collides, very simple, keys are short at first | The counter is a single point to scale; keys are guessable (1, 2, 3 …) |
| Pre-generated key pool | A separate service fills a table of unused random keys; app servers take blocks of them | No collisions, no live coordination on every write, not guessable | An extra service to run; unused keys in a crashed server’s block are lost |
| Random key + check | Pick 7 random base62 characters, insert, retry if it exists | Simple, unguessable | At very high fill, more retries |
A good answer in an interview: counter with ranges. Each app server asks a small coordinator (or a database sequence) for a block, say 1,000,000 numbers, and then hands them out locally with no further coordination. It never collides, scales to many servers, and a crash only loses part of one block. If guessability matters, shuffle the bits of the number before encoding, or use the pre-generated random pool instead.
Step 5: Design the API
POST /api/v1/urls
{ "long_url": "https://example.com/a/very/long/path", "alias": "my-link" (optional), "expires_at": "2027-01-01" (optional) }
-> 201 { "short_url": "https://sho.rt/aZ3kP9x" }
GET /aZ3kP9x
-> 301 or 302, Location: https://example.com/a/very/long/path
Validate the long URL (scheme, length, not your own domain to avoid redirect loops), and rate-limit the create endpoint per user or per IP.
Step 6: The architecture
The read path (the hot one)
- The app server receives
GET /aZ3kP9x. - It looks up the key in the cache. A hit returns the long URL in under a millisecond or two.
- On a miss it reads the database, writes the result into the cache, and redirects.
- Unknown keys return 404 (and cache the “not found” for a short time so scanners do not hammer the database).
The write path
- Validate the request and apply the rate limit.
- Take the next number from the server’s block, encode it in base62, or use the custom alias after checking it is free.
- Insert
(key, long_url, created_at, expires_at, owner)into the database. The key is the primary key, so a duplicate fails loudly instead of overwriting. - Return the short URL.
Step 7: Pick the data store
The access pattern is one lookup by key, plus an insert. There are no joins. That fits a key-value store or a plain SQL table with the key as primary key, partitioned (sharded) by the key. Six billion small rows is within what either can handle once sharded. Choose what your team can run well. Add read replicas for the read load, and make sure the database can fail over without losing acknowledged writes.
Step 8: 301 or 302?
| Code | Meaning | Effect |
|---|---|---|
| 301 | Moved permanently | Browsers and search engines may cache it, so repeat visits skip your server. Lower load, but you lose some click counts and cannot change the target easily. |
| 302 | Found (temporary) | Every click comes to you. Accurate analytics and editable links, at the cost of more traffic. |
If you sell click analytics or need links you can edit or expire, use 302. If you only care about speed and load, 301 is fine. Say which you chose and why.
Step 9: Analytics without slowing redirects
Do not write a click count to the database inside the redirect request. Instead, push a small event (key, time, country, device) onto a queue and return the redirect immediately. A separate worker batches these events into counters or a column store. If the queue is down, you lose a few clicks, not the redirect.
Step 10: The edges that interviewers ask about
- Expiry. Store
expires_at. Check it on read, and run a background job to delete old rows in small batches. - Custom aliases. Make them a separate namespace or reject aliases that look like generated keys, so a user cannot grab a key your generator will produce later.
- Abuse. Shorteners are used for phishing. Check destinations against a blocklist, rate-limit creation, and keep a way to disable a key quickly.
- Hot keys. One viral link can overload a single cache node. Replicate very hot keys across cache nodes, or keep a small in-process cache on each app server.
- Multi-region. Keep a copy of the data in each region and route users to the nearest. Writes can go to one primary region and replicate out, because writes are rare.
- Deleting a link. Remove it from the database and invalidate the cache entry, or the old redirect keeps working until the cache expires.
Common mistakes
- Spending the whole time on the redirect and never mentioning how keys are generated and kept unique.
- Using auto-increment ids from a single database server as the keys without saying how it scales or that the keys are guessable.
- Updating click counts synchronously on every redirect.
- Forgetting that a cache needs an eviction policy and an answer for “what if the cache is down?” (the database must be able to take the load, or you must shed it gracefully).
- Giving numbers without saying they are assumptions.
How to practise this
Redraw the diagram from memory, redo the estimate with different assumptions (10× the traffic, or a 1,000:1 read ratio), and explain each choice out loud. The building blocks used here each have their own question sets on this site: caching strategies, database indexing, message queues and microservices. For a structured plan, see the System Design Prep page.
Frequently asked questions
Should I hash the URL or use a counter?
Both work. A hash gives the same key for the same URL but needs collision handling. A counter in blocks never collides and is simple to scale. For most designs I would explain the counter-with-ranges approach and mention the hash as an alternative.
How long should the short key be?
Work it out from your key space: 626 is about 56.8 billion and 627 is about 3.5 trillion. If you expect billions of links, 7 characters leaves plenty of room.
Do I need a cache if the database is fast?
Yes, for latency and load. Most redirects hit a small set of popular links, so a cache removes most database reads and makes each redirect faster.
Is SQL or NoSQL better here?
Neither is required. The access pattern is a lookup by key, which suits a key-value store, but a sharded SQL table works too. Choose by what your team can operate reliably.
What is the single hardest part?
Producing unique keys across many servers without a bottleneck. Everything else is standard caching, storage and queues.
Note: this is a teaching design with assumed numbers, not a description of a service I run. Real systems differ in details. Use it to learn the method, then adapt the numbers to the question you are given.