One interview classic a day

System Design Daily

Each morning, one classic junior-level system design question, built block by block: requirements, API, a simple design that works, and the follow-ups interviewers like to ask.

Design a URL shortener

You paste a long link and get back something tiny like sho.rt/x7Kp2Q. Anyone who opens the tiny link lands on the original page. It is the classic first system design question: small enough for one interview, yet it still covers unique IDs, caching and staying online.

Legend 10 terms used below
Short code
The few characters at the end of a short link, like x7Kp2Q. It is the key we use to find the original URL.
Redirect
A reply from a server that says "what you want is over there" and gives the browser a new address to load instead.
Availability
How much of the time a system is up and answering. "Highly available" means it almost never goes down.
Endpoint
One address plus HTTP method that a server answers, like POST /urls. Together, the endpoints make up the API.
Load balancer
A machine that receives all incoming requests and spreads them across several app servers, so no single one gets overloaded.
Primary key
The column a table is organised by. The database keeps an index on it, so finding a row by its key is fast, even among billions.
Base62
Writing a number with 62 symbols (0-9, a-z, A-Z) instead of 10 digits, so big numbers turn into short strings.
Cache
A small, very fast store that keeps copies of popular data in memory, so most requests can skip the slower database.
CDN
Content delivery network: servers spread around the world that keep copies of answers close to users, so responses travel less distance.
Replica
A copy of the database that is kept up to date with the main one. If the main one dies, a replica can take over.

Functional requirements

  • Submit a long URL and get a short one back.
  • Opening the short URL takes you to the original site.
  • Optional: pick a custom alias (sho.rt/my-party) or an expiry date.

Out of scope: user accounts, click analytics, spam and malware filtering.

Non-functional requirements

  • Unique short codes. One code must point to exactly one long URL, or people land on the wrong site.
  • Fast redirects. A short link stands between a person and the page they want, so the extra hop should feel instant (a common target is under about 100 ms).
  • High availability. If it goes down, every short link ever shared breaks at once. A brand-new link may take a moment to work everywhere; old links must keep working.
  • Built for reads. A link is created once but may be opened thousands of times, so reads far outnumber writes.

Core entities

  • Link: the short code, the original long URL, when it was made and (optionally) when it expires.
  • User: who created the link. Only needed once we add accounts, which is out of scope today.

API design

One endpoint per functional requirement is enough.

POST/urls

Create a short link. Alias and expiry are optional.

Request body
{
  "long_url": "https://example.com/a/long/page",
  "custom_alias": "my-party",
  "expires_at": "2026-12-31"
}
Response 201 Created
{
  "short_url": "https://sho.rt/x7Kp2Q"
}
GET/{short_code}

Follow a short link. This is what a browser sends when someone clicks it.

Request
GET /x7Kp2Q
Response 302 Found
Location: https://example.com/a/long/page

404 if the code is unknown
410 Gone if it has expired

High-level design

We build it one requirement at a time, simplest version first. I'll assume the usual setup: a few identical app servers that keep no data of their own, behind a load balancer.

1. Creating a short link

The client sends POST /urls. An app server checks the long URL looks valid, makes a new short code (first deep dive), saves the pair in the database and returns the short link.

Table: links
ColumnTypeNote
short_codetextprimary key, e.g. x7Kp2Q
long_urltextwhere the link goes
created_attimestampwhen it was made
expires_attimestamp, optionalempty means never

Because short_code is the primary key, finding a link by its code stays fast even with billions of rows.

Do we need several databases? Say 100M new links a month, which is about 40 per second. At roughly 500 bytes a row, that is 50 GB a month, or about 600 GB a year. One database server handles both easily, so we don't split the data yet.

URL shortener architecture A client sends requests to a load balancer, which passes them to identical app servers. The app servers ask a counter for new numbers when creating links, check a cache first when following links, and read and write a database that has a replica. Clientbrowser / app Loadbalancer Appserversidentical, no data Counternext number Cachepopular codes Databaseprimary + replica new code check first read / write gold = create a link cyan = follow a link
Identical app servers in the middle, a counter for new codes, a cache for fast lookups, and a database (plus replica) holding every link.

2. Following a short link

Someone opens sho.rt/x7Kp2Q, so their browser sends GET /x7Kp2Q. The app server looks up the code and replies with status 302 and a Location header holding the long URL. The browser follows it on its own. Unknown codes get 404, expired ones get 410.

Following a short link, step by step The browser asks the app server for the short code. The app server asks the cache, which returns the long URL; on a miss the server reads the database and fills the cache. The app server answers with a 302 redirect, and the browser then loads the original site. Browser App server Cache Original site 1. GET /x7Kp2Q 2. look up x7Kp2Q 3. long URL (hit) miss? read the database, then fill the cache 4. 302 + Location 5. browser loads the long URL itself
One click, five steps. We only handle steps 1 to 4, and the cache answers most lookups.

Deep dives

How do we keep short codes unique?

We need a short string nobody else already has. Three common approaches:

  1. Use part of the long URL. Take its first few characters. Many URLs start with https://www., so codes clash constantly. Not workable.
  2. Hash the long URL. Run it through a hash function such as SHA-256, write the result in base62, and keep the first 7 characters. The same URL always gives the same code. But two different URLs can occasionally share those 7 characters, so we must check the database and retry with a tweak when that happens.
  3. Count.Pick One counter hands out the next number: 1, 2, 3 and so on. Each new link takes a number and writes it in base62, so 125 becomes 21 (2 × 62 + 1). Two links can never get the same number, so there is nothing to check. An in-memory store like Redis can increase a counter safely for this, even with many app servers asking at once.

I'd pick the counter, because uniqueness comes from the design itself, with no retry logic. Is 7 characters enough? 627 is about 3.5 trillion codes. At 100M links a month we use about 1.2 billion a year, so we would last thousands of years.

The downside: codes come out in order, so neighbouring links are guessable. If that matters, scramble the number with a reversible step before encoding. One counter is no bottleneck at about 40 new numbers a second, and the unique primary key stays as a safety net if it ever repeats a number after a crash.

How do we keep redirects fast?

Every click is a read. If each link is opened about 100 times, our 40 new links a second become about 4,000 redirects a second on average, and several times that at busy moments.

  1. Just the database. A lookup by primary key already takes only a few milliseconds. Fine to start, but every click lands on the database.
  2. Add a cache in front.Pick Keep popular code-to-URL pairs in a cache such as Redis. The app server checks the cache first; on a miss it reads the database and saves the answer for next time. Memory lookups take well under a millisecond, and clicks tend to bunch up on a small set of popular links, so most requests never reach the database.
  3. Cache close to users. Answer redirects from a CDN. Fastest, but harder to expire links or count clicks. A "later" idea.

When a link expires or changes, delete its cache entry too, and never let a cached entry outlive the link's own expiry date.

How do we stay available?

App servers are interchangeable, so losing one is no big deal. The scary failure is the database: if the only copy dies, every redirect fails.

  1. One database plus backups. Cheap, but restoring can take hours, with every link broken meanwhile.
  2. A primary with replicas.Pick Every write goes to the primary and is copied to one or two replicas. If the primary dies, a replica is promoted to take its place. Replicas can also answer redirect lookups.
  3. Copies in several regions. Survives a whole region going offline, at a big cost in complexity. Later, if ever.

Replicas lag a moment behind, so a brand-new link might be missing from one for a fraction of a second. That is the trade we accepted: availability first, perfectly fresh data second.

Final design

Putting the deep dives together gives the complete picture: a counter for unique codes, a cache for fast redirects, and a replica so one database failure doesn't break every link. One change from the first sketch: an API gateway, the single front door that sends each request to the right service, splits the work into a write service and a read service. Clicks vastly outnumber new links, so the read side can be run and scaled on its own.

Final URL shortener design A client talks to an API gateway, which routes POST /urls to a write service and GET short-code requests to a read service. The write service gets the next number from a Redis counter, encodes it in base62 and saves the row in the primary database, which copies every write to a replica. The read service checks a Redis cache first, reads the replica on a miss, and replies with a 302 redirect. The database holds the links table; the cache holds short code to long URL entries. Clientbrowser APIgateway- one front door- routes eachrequest by path POST /urls GET /{code} Write service- get the next number- base62 it, save row Read service- cache, else database- reply 302 redirect CounterRedis INCR next number Databaseprimary save row links tableshort_code PKlong_urlcreated_atexpires_at optional Replicacopy of primary copies writes on miss CacheRedis, in memory check first cache entrykey short_codevalue long_urlTTL <= link expiry
The complete system: separate write and read services behind one gateway. Each block says what it does, and the signs show what each store holds. Gold = creating a link, cyan = following one.

Interview tips

Why 302 instead of 301?
A 302 keeps every click coming through us, so we can expire, change or count links. Browsers cache a 301 and stop asking.
Two people shorten the same URL. Same code?
Not necessarily. A new code each time is simpler; de-duplicate only if asked (hashing gives it for free).
How do custom aliases work?
Check the alias is free (the unique primary key enforces it), and keep aliases in their own space, like sho.rt/c/my-party, so a generated code can never land on one.

Common mistakes

  • Jumping to sharding, queues and microservices before the basic create and redirect flow works.
  • Ignoring the read/write imbalance. Reads outnumber writes by a lot, so the cache is where design effort pays off.

Sources: Hello Interview, Design a URL shortener like Bit.ly and its delivery framework; Alex Xu, System Design Interview (Vol. 1), URL shortener chapter; MDN, 302 Found.