# Design a URL Shortener | System Design Question

Time to put the framework to work on a real problem. The URL shortener - TinyURL, Bitly - is the [classic first system design question](/hld-vs-lld-common-system-design-questions) because it is small enough to finish in an hour yet exercises every step. We will walk it end to end, starting the way a real interview does: with the questions you ask and the answers you would get back, before a single box is drawn.

## Step 1: Ask questions, and be ready for messy answers

The prompt "design a URL shortener" is vague on purpose, and here is the part the polished write-ups skip: **you will not always get clean answers.** A strong interviewer hands you numbers; a distracted or inexperienced one says "you decide," gives a fuzzy reply, or bolts on a surprise feature halfway through. Your job is to [ask anyway](/the-first-thing-you-should-do-in-a-system-design-interview) and keep the problem moving no matter what comes back. A realistic exchange, punts and all:

> **You:** Quick example - what does the service actually do?
> **Interviewer:** A long URL comes in, you return a short alias, and clicking the alias redirects to the original.
>
> **You:** Roughly what scale - creates per day, or per second?
> **Interviewer:** Eh, you tell me.

That "you tell me" is where people freeze. Don't - **propose a number, say it out loud, and write it down:** "I'll assume 100 million new URLs a day, about 1,000 writes per second, and design for that - stop me if you had a very different scale in mind." Now you own the scale instead of waiting for it. Scale is the one thing you cannot leave unset: "5 GB once a day" and "5 PB per second" are completely different systems.

> **You:** How short should the code be, and which characters can it use?
> **Interviewer:** As short as possible. Alphanumeric is fine.

Alphanumeric means 62 characters (base-62) - and it quietly rules out base64, whose `+` and `/` are not URL-safe. "As short as possible" you will turn into a concrete length during the estimate.

> **You:** If two people shorten the same long URL, same code or different?
> **Interviewer:** Same code - dedupe them.

That one decision shapes your write path: dedupe means a lookup on every create. Had they said "different every time," you would skip that lookup entirely.

> **You:** Anything beyond shorten and redirect - custom aliases, expiry, analytics?
> **Interviewer:** Actually, yeah - let's support custom aliases and click counts.

There is the curveball. A less experienced interviewer will cheerfully pile on scope, so keep the one-line cost of each ready: a **custom alias** adds a uniqueness check on a user-supplied code; **click analytics** adds an async events pipeline and nudges you toward a 302 redirect (which routes every click through your service) over a 301. You do not design them now - you name the cost and park them: "Got it - alias needs a collision check on user input, analytics needs an events pipeline; I'll get the core working first and fold those in if we have time."

The rule underneath all of it: **when the interviewer answers, use it; when they punt, make a reasonable assumption and state it; when they add scope, name its cost and keep going.** Never stall waiting for a tidy answer that may not come.

With that settled, write the scope as precise points - not prose:

**Functional requirements**

- Create a short URL from a given long URL.
- Redirect a short URL to the original long URL.
- Short codes are alphanumeric: `[0-9, a-z, A-Z]`.
- The same long URL maps to one short code (dedupe on create).
- Links are immutable - no edit, no delete.
- Custom aliases and click analytics: acknowledged, deferred.

**Non-functional requirements**

- Read-heavy: redirects outnumber creates roughly 10:1.
- Low redirect latency (target under ~100 ms).
- High availability - the redirect path is critical (~99.9%+).
- Short codes are unique and not trivially guessable in bulk.
- Storage scales to hundreds of billions of records.

**A quick estimate** turns the scale into numbers. From the 100 million a day you assumed:

```text
Write QPS = 100,000,000 / 100,000 (seconds/day) = ~1,000/s
Read QPS  = 1,000 x 10                            = ~10,000/s
Records in 10 years = 100M x 365 x 10             = ~365 billion
```

Two facts fall out immediately and shape everything: the system is heavily **read-heavy** (so cache the redirect path), and it must store **hundreds of billions of records** (so the store has to scale). Write throughput at ~1,000/s is modest.

## Step 2: High-level design

**The API** is just two REST endpoints:

```text
POST /api/v1/data/shorten   body: { longUrl }        -> returns shortUrl
GET  /{shortUrl}            -> HTTP redirect to longUrl
```

**The architecture** is a stateless web tier behind a load balancer, a database for the mappings, a cache in front of reads, and a unique-ID generator that the shortening path uses (more on that in the deep dive).

![High-level architecture of a URL shortener: client to load balancer to stateless web servers, backed by a Redis cache, a URL database, and a unique ID generator](https://mermaid.ink/img/Zmxvd2NoYXJ0IExSCiAgICBDbGllbnQgLS0+fHNob3J0ZW4gYW5kIHJlZGlyZWN0fCBMQltMb2FkIEJhbGFuY2VyXQogICAgTEIgLS0+IFcxW1dlYiBzZXJ2ZXJdCiAgICBMQiAtLT4gVzJbV2ViIHNlcnZlcl0KICAgIFcxIC0tPiBDYWNoZVsoUmVkaXMgY2FjaGUpXQogICAgVzEgLS0+IERCWyhVUkwgZGF0YWJhc2UpXQogICAgVzEgLS0+IElER1tVbmlxdWUgSUQgZ2VuZXJhdG9yXQ==?type=png&bgColor=FFFFFF)

**The data model** is trivial - a single table of mappings, so a relational database is a fine starting point:

```text
url ( id BIGINT PRIMARY KEY, short_url VARCHAR, long_url VARCHAR )
```

**301 vs 302 redirect** is the one API subtlety worth raising. A [301 (permanent)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301) tells the browser to cache the redirect, so future clicks skip your service entirely and go straight to the long URL - great for reducing server load. A 302 (temporary) routes every click through your service first - worse for load, but it lets you track click analytics. Pick 301 to offload traffic, 302 if analytics matter.

## Step 3: Deep dive - generating the short code

This is the heart of the problem and where the interviewer wants depth: how do you turn a long URL into a unique 7-character code?

**Why 7 characters?** With 62 possible characters per position, a code of length `n` gives `62^n` combinations. We need to cover 365 billion URLs, and `62^7 ≈ 3.5 trillion`, comfortably more than enough - so 7 characters it is.

There are two standard approaches.

**Option A - hash + collision resolution.** Hash the long URL (CRC32, MD5, SHA-1) and take the first 7 characters. Simple, but two different URLs can collide on the same 7 chars. You resolve collisions by appending a predefined string and re-hashing until the code is free - which means a database check on every shorten, and a [Bloom filter](/concepts-you-should-know-about-system-designing-part-1) to make "does this code exist?" cheap.

**Option B - base-62 conversion (the cleaner choice).** Generate a unique numeric ID, then convert that number to base 62. Because each ID is unique, the resulting code is unique by construction - no collisions, no lookups to check.

![Base-62 conversion: a unique numeric ID like 2009215674938 is encoded using the 62 characters 0-9 a-z A-Z into the short code zn9edcu, giving short.ly/zn9edcu](https://mermaid.ink/img/Zmxvd2NoYXJ0IExSCiAgICBBW1VuaXF1ZSBJRDogMjAwOTIxNTY3NDkzOF0gLS0+IEJbQmFzZS02MiBlbmNvZGU6IDAtOSBhLXogQS1aXQogICAgQiAtLT4gQ1tTaG9ydCBjb2RlOiB6bjllZGN1XQogICAgQyAtLT4gRFtzaG9ydC5seS96bjllZGN1XQ==?type=png&bgColor=FFFFFF)

The catch base-62 pushes onto you: you now need **globally unique IDs** in a distributed system. That is its own well-known sub-problem (a Snowflake-style distributed ID generator, or a database auto-increment for smaller scale) - worth naming so the interviewer knows you see it.

**The shortening (write) flow** ties it together, with a dedup check so the same long URL is not stored twice:

![Shorten flow: client POSTs a long URL; the server checks the database for an existing mapping and returns it if present; otherwise it gets a new unique ID, base-62 encodes it, stores the mapping, and returns the short URL](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDIGFzIENsaWVudAogICAgcGFydGljaXBhbnQgUyBhcyBXZWIgU2VydmVyCiAgICBwYXJ0aWNpcGFudCBEQiBhcyBEYXRhYmFzZQogICAgcGFydGljaXBhbnQgSURHIGFzIElEIEdlbmVyYXRvcgogICAgQy0+PlM6IFBPU1QgL3Nob3J0ZW4gd2l0aCBsb25nVXJsCiAgICBTLT4+REI6IGRvZXMgbG9uZ1VybCBhbHJlYWR5IGV4aXN0PwogICAgYWx0IGFscmVhZHkgc2hvcnRlbmVkCiAgICAgICAgREItLT4+UzogZXhpc3Rpbmcgc2hvcnRVcmwKICAgIGVsc2UgbmV3IHVybAogICAgICAgIERCLS0+PlM6IG5vdCBmb3VuZAogICAgICAgIFMtPj5JREc6IG5leHQgdW5pcXVlIGlkCiAgICAgICAgSURHLS0+PlM6IDIwMDkyMTU2NzQ5MzgKICAgICAgICBOb3RlIG92ZXIgUzogYmFzZTYyIG9mIGlkIGlzIHpuOWVkY3UKICAgICAgICBTLT4+REI6IHN0b3JlIGlkLCBzaG9ydFVybCwgbG9uZ1VybAogICAgZW5kCiAgICBTLS0+PkM6IHNob3J0Lmx5L3puOWVkY3U=?type=png&bgColor=FFFFFF)

**The redirect (read) flow** is where the read-heavy requirement pays off. Because redirects dominate 10:1, we put a cache in front of the database: check the cache first, fall back to the database on a miss, and populate the cache for next time. Most redirects never touch the database at all.

![Redirect flow: client requests a short code; the load balancer forwards to a web server, which checks Redis first and returns the long URL on a hit, or falls back to the database on a miss (repopulating the cache), then issues a 301 redirect](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDIGFzIENsaWVudAogICAgcGFydGljaXBhbnQgTEIgYXMgTG9hZCBCYWxhbmNlcgogICAgcGFydGljaXBhbnQgUyBhcyBXZWIgU2VydmVyCiAgICBwYXJ0aWNpcGFudCBDYWNoZSBhcyBSZWRpcwogICAgcGFydGljaXBhbnQgREIgYXMgRGF0YWJhc2UKICAgIEMtPj5MQjogR0VUIC96bjllZGN1CiAgICBMQi0+PlM6IGZvcndhcmQKICAgIFMtPj5DYWNoZTogZ2V0KHpuOWVkY3UpCiAgICBhbHQgY2FjaGUgaGl0CiAgICAgICAgQ2FjaGUtLT4+UzogbG9uZ1VybAogICAgZWxzZSBjYWNoZSBtaXNzCiAgICAgICAgQ2FjaGUtLT4+UzogbmlsCiAgICAgICAgUy0+PkRCOiBsb29rIHVwIHpuOWVkY3UKICAgICAgICBEQi0tPj5TOiBsb25nVXJsCiAgICAgICAgUy0+PkNhY2hlOiBzZXQoem45ZWRjdSwgbG9uZ1VybCkKICAgIGVuZAogICAgUy0tPj5DOiAzMDEgcmVkaXJlY3QgdG8gbG9uZ1VybA==?type=png&bgColor=FFFFFF)

## Step 4: Wrap up

With the core designed, name the improvements you would make with more time - this is where you show you see past the happy path:

- **Rate limiting.** A malicious client could flood the shorten endpoint; throttle by IP or API key.
- **Scale the web tier.** It is stateless, so scaling is just adding servers behind the load balancer.
- **Scale the database.** ~365 billion rows means replication for read capacity and sharding (by the short code) for storage.
- **Analytics.** Click counts, referrers, and geography are a common follow-on - and the reason you might choose 302 over 301.
- **Availability and consistency.** Since a stale-but-available redirect is fine, this system leans toward availability; the mapping is immutable once written, which makes caching and replication easy.

## Follow-up questions to expect

Once the core is on the board, the interviewer starts probing - this is where a lot of the signal is. Have crisp, reasoned answers ready:

- **"What if the unique ID generator dies?"** It is the one component the write path cannot do without, so I would not run it as a single node. A Snowflake-style generator gives each node its own machine id, so several run independently with no coordination, and I could hand each app server a pre-allocated id range as a fallback. Redirects never touch it, so reads stay up regardless.
- **"A link goes viral - millions of hits a minute. What breaks?"** Nothing new: that code is already hot in the cache, so it serves from memory. I would add cache replicas for headroom and, because the mapping is immutable, push it to a CDN or edge cache so the redirect resolves near the user without ever reaching our servers.
- **"How would custom aliases work?"** The user supplies the code, so I need a uniqueness check before saving - a conditional write against the same key space as generated codes - and I reject it and ask for another if it is already taken.
- **"How do links expire?"** Add an `expires_at` field, set a matching TTL on the cache entry, and let a background job sweep expired rows; a redirect past expiry returns `410 Gone`.
- **"Are the short codes guessable?"** With sequential ids, base-62 codes are enumerable - someone could walk the whole space and scrape every link. If that matters, I would randomize or hash the id before encoding, trading a character or two of length for unpredictability.
- **"Cache and database consistency?"** This design gets lucky here: a mapping never changes once written, so the cache cannot go stale - there is no invalidation problem, only a cold-cache miss that loads the value once.
- **"How do you scale this to 10x?"** Almost nothing structural changes, which is the whole point: the stateless web tier adds instances, the cache adds replicas, the database shards further by short code, and the ID generator adds nodes.

## What you just built

A URL shortener is the ideal first walkthrough because the shape generalizes: scope and estimate, sketch a stateless tier with a cache and a database, then deep-dive the one hard part - here, unique short-code generation via base-62 over a distributed ID. Reads dominate, so caching carries the load; writes are simple; storage is the thing that has to scale.

Next in the series we take on a read-heavy problem where the deep dive is harder - a news feed - and the fanout question from earlier posts comes back in full. The framework followed here is from Alex Xu's [System Design Interview](https://bytebytego.com).

