AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content
SD notes

System Design · Note 13 of 22

Design a URL Shortener

Intermediate · 8 min read · updated 29 October 2026

Quick answer

How do you design a URL shortener?

Generate a unique ID for each long URL, encode it in base62 to get a short code of six or seven characters, and store code and URL in a key-value store. On a read, look the code up in a cache, falling back to the database, and answer with an HTTP 301 or 302 redirect. Reads far outnumber writes, so caching and replication carry the load.

On this page
  1. Requirements
  2. Capacity estimate
  3. API
  4. Write path
  5. Read path
  6. 301 vs 302
  7. Scaling it
  8. Interview questions
  9. Common questions

The classic first system design question: small to state, and it touches estimation, ID generation, storage, caching and HTTP.

Requirements

  • Functional: shorten a long URL; redirect a short code to it; optional custom alias and expiry.
  • Non-functional: redirects fast and highly available; codes unique and not easily guessed (if required).
  • Read-heavy: a link is created once and opened many times.

Capacity estimate

  • State the assumptions, then derive QPS, storage and the code length.
  • The figures below follow from these assumptions; change them and the numbers move.
Capacity estimate: from assumptions to QPS, storage and code length

Start from stated assumptions, then derive the numbers the design must meet. Say the assumptions out loud; an interviewer may change them.

0:01 / 0:19Step 1 of 7
Transcript · 7 steps

API

HTTP
Copy
POST /api/shorten        {"url": "https://shop.example/item/48213"}  → 201 {"code": "2bYsMq"}
GET  /2bYsMq             → 302  Location: https://shop.example/item/48213

Write path

  • Counter + base62: a unique number per URL, encoded in 62 characters.
  • No collisions by construction; the ID source must be shared or partitioned.
  • Hand each server a range of IDs so it does not ask on every write.
Write path: long URL → unique id → base62 code → database

POST /shorten carries a long URL. The API server asks a counter for the next unique id, encodes it in base62 and stores the pair.

0:01 / 0:15Step 1 of 5
Transcript · 5 steps
Base62: turning a numeric id into a short code

To encode id 2,009,215,674, divide by 62 again and again. Each remainder, 0–61, picks one character from 0–9, a–z, A–Z.

0:01 / 0:22Step 1 of 8
Transcript · 8 steps

Counter vs hash

Counter + base62Hash + truncate
CollisionsNonePossible — check and retry
CoordinationShared ID sourceNone
Same URL twiceNew code each timeSame code (dedupes)
GuessableSequential unless shuffledNo

Read path

  • Look the code up in a cache (Redis, memcached) first; database on a miss.
  • Popular links stay hot, so most reads never reach the database.
  • Unknown code → 404; expired → 410 Gone.
Read path: cache first, database on a miss, then a redirect

A browser opens short.example/<code>. The server looks the code up in an LRU cache of 2 entries first and goes to the database only on a miss, then redirects.

0:01 / 0:24Step 1 of 8
Transcript · 8 steps

301 vs 302

  • 301 Moved Permanently: browsers cache it; fewer requests reach you, but you lose click counts and cannot change the target.
  • 302 Found: every click comes back; analytics, edits and expiry work.
  • Choose 302 when analytics matter, 301 when load matters most.

Scaling it

Interview questions

  • How many characters does the code need? Estimate total rows, then the smallest n with 62^n above it.
  • How do you avoid two servers issuing the same ID? Pre-allocated ID ranges per server, or Snowflake-style IDs.
  • Where is the bottleneck? Reads; a cache in front of a sharded key-value store absorbs them.
  • 301 or 302? 302 if you need click analytics or editable links; 301 to shed load.

Next: Back-of-the-Envelope Estimation · Before this: Microservices and API Gateway

Common questions

How long should a short code be?

Long enough for every URL you will ever store. With 62 characters, 6 characters give about 56.8 billion codes and 7 give about 3.5 trillion. Estimate the total rows over the system's life and pick the smallest length whose 62^n covers it, with room to spare.

Should I use a counter or a hash for the short code?

A counter encoded in base62 never collides and gives the shortest codes, but needs a distributed ID source and makes codes guessable in order. Hashing the URL and taking the first characters needs no coordination, but truncated hashes collide, so every write must check and retry.

Should a URL shortener use 301 or 302?

A 301 is permanent, so browsers and caches remember it and later clicks never reach your servers — less load but no click counts. A 302 is temporary, so every click comes back, which costs more traffic but lets you count clicks, change the target or expire the link.

Which database suits a URL shortener?

The data is one large table read by its key with no joins, so a key-value or wide-column store such as DynamoDB or Cassandra fits and shards easily. A relational database with an index on the code also works at moderate scale.

How do you generate unique IDs across many servers?

Give each server a range of IDs from a coordinator such as ZooKeeper or a database sequence, so it hands them out locally until the range runs out. Snowflake-style IDs, made of a timestamp, a machine number and a sequence, are another common choice.

Design a URL Shortener: System Design Case Study

Design a URL shortener like TinyURL, step by step: requirements, capacity estimate, base62 IDs, the write and read paths, caching and 301 vs 302 redirects.

  • Subject: System Design
  • Level: Intermediate
  • Reading time: 8 min
  • Updated: 2026-10-29

How do you design a URL shortener?

Generate a unique ID for each long URL, encode it in base62 to get a short code of six or seven characters, and store code and URL in a key-value store. On a read, look the code up in a cache, falling back to the database, and answer with an HTTP 301 or 302 redirect. Reads far outnumber writes, so caching and replication carry the load.

The classic first system design question: small to state, and it touches estimation, ID generation, storage, caching and HTTP.

Requirements

  • Functional: shorten a long URL; redirect a short code to it; optional custom alias and expiry.
  • Non-functional: redirects fast and highly available; codes unique and not easily guessed (if required).
  • Read-heavy: a link is created once and opened many times.

Capacity estimate

  • State the assumptions, then derive QPS, storage and the code length.
  • The figures below follow from these assumptions; change them and the numbers move.
assumptionsestimatenew URLs / month100Mreads per write100bytes per row500kept for5 yearswrites / second100M ÷ 2,592,000 s≈ 39/sreads / second38.6 × 100≈ 3,900/srows in 5 years100M × 12 × 56 billionstorage6B × 500 B3 TBcode length62^5 < 6B ≤ 62^66 chars
Capacity estimate: from assumptions to QPS, storage and code length.
  1. Start from stated assumptions, then derive the numbers the design must meet. Say the assumptions out loud; an interviewer may change them.
  2. Writes: 100 million a month over 2,592,000 seconds is about 38.6 a second — small for one database.
  3. Reads: 100 times that is about 3,858 a second on average, more at peaks. Reads dominate, so the design is built around a cache.
  4. Rows: 100 million a month for 5 years is 6 billion short links to keep.
  5. Storage: 6 billion rows at 500 bytes is 3 TB before replicas — more than one cheap disk, so plan to shard or use a store that does.
  6. Code length: 62^5 is only about 916 million, while 62^6 is about 57 billion, so 6 base62 characters are enough.
  7. The design: about 39 writes and 3,900 reads a second, 3 TB over 5 years, and 6-character codes — a read-heavy system.

API

POST /api/shorten        {"url": "https://shop.example/item/48213"}  → 201 {"code": "2bYsMq"}
GET  /2bYsMq             → 302  Location: https://shop.example/item/48213

Write path

  • Counter + base62: a unique number per URL, encoded in 62 characters.
  • No collisions by construction; the ID source must be shared or partitioned.
  • Hand each server a range of IDs so it does not ask on every write.
ClientAPI serverID generatornext id 2,009,215,677Databaseidcodelong URL2,009,215,6742bYsMqshop.example/item/48213?ref=mail2,009,215,6752bYsMrblog.example/2026/10/caching-guide2,009,215,6762bYsMsdocs.example/api/v2/auth#tokens3
Write path: long URL → unique id → base62 code → database.
  1. POST /shorten carries a long URL. The API server asks a counter for the next unique id, encodes it in base62 and stores the pair.
  2. URL 1 reaches the API server, which takes id 2,009,215,674 and encodes it as 2bYsMq.
  3. URL 1 is stored under 2bYsMq and the client gets short.example/2bYsMq. URL 2 reaches the API server, which takes id 2,009,215,675.
  4. URL 2 is stored under 2bYsMr and the client gets short.example/2bYsMr. URL 3 reaches the API server, which takes id 2,009,215,676.
  5. URL 3 is stored as 2bYsMs. Consecutive ids give distinct codes with no collision check; the price is codes that are easy to guess in order.
number÷ 62 =remchar2,009,215,67432,406,70426q32,406,704522,68848M522,6888,43028s8,43013560Y135211b2022code, read bottom-up2bYsMq
Base62: turning a numeric id into a short code.
  1. To encode id 2,009,215,674, divide by 62 again and again. Each remainder, 0–61, picks one character from 0–9, a–z, A–Z.
  2. 2,009,215,674 ÷ 62 = 32,406,704, remainder 26, which is "q" (a lower-case letter). The quotient is divided next.
  3. 32,406,704 ÷ 62 = 522,688, remainder 48, which is "M" (an upper-case letter). The quotient is divided next.
  4. 522,688 ÷ 62 = 8,430, remainder 28, which is "s" (a lower-case letter). The quotient is divided next.
  5. 8,430 ÷ 62 = 135, remainder 60, which is "Y" (an upper-case letter). The quotient is divided next.
  6. 135 ÷ 62 = 2, remainder 11, which is "b" (a lower-case letter). The quotient is divided next.
  7. 2 is under 62, so the quotient is 0 and this is the last digit: remainder 2 is "2". The division stops.
  8. Read the characters from the last division to the first: 2,009,215,674 is "2bYsMq". 6 characters cover every id below 62^6, about 57 billion.

Counter vs hash

Counter + base62Hash + truncate
CollisionsNonePossible — check and retry
CoordinationShared ID sourceNone
Same URL twiceNew code each timeSame code (dedupes)
GuessableSequential unless shuffledNo

Read path

  • Look the code up in a cache (Redis, memcached) first; database on a miss.
  • Popular links stay hot, so most reads never reach the database.
  • Unknown code → 404; expired → 410 Gone.
BrowserApp serverCacheLRU · 2 entriesDatabase2bYsMr2bYsMqmost recent → older
Read path: cache first, database on a miss, then a redirect.
  1. A browser opens short.example/<code>. The server looks the code up in an LRU cache of 2 entries first and goes to the database only on a miss, then redirects.
  2. GET /2bYsMq: a miss, so the server reads the database, caches the row and answers 302.
  3. GET /2bYsMr: a miss, so the server reads the database, caches the row and answers 302.
  4. GET /2bYsMq: a cache hit. The server answers 302 with the long URL without touching the database, and 2bYsMq becomes the most recent entry.
  5. GET /2bYsMs: a miss, so the server reads the database, caches the row (evicting 2bYsMr, the least recently used) and answers 302.
  6. GET /2bYsMq: another hit — popular codes stay at the front of the cache, so the database is not read again for 2bYsMq.
  7. GET /2bYsMr: a miss, so the server reads the database, caches the row (evicting 2bYsMs, the least recently used) and answers 302.
  8. 2 of 6 redirects came from the cache. Real traffic is skewed towards a few popular links, so a cache holding a small share of codes serves most reads.

301 vs 302

  • 301 Moved Permanently: browsers cache it; fewer requests reach you, but you lose click counts and cannot change the target.
  • 302 Found: every click comes back; analytics, edits and expiry work.
  • Choose 302 when analytics matter, 301 when load matters most.

Scaling it

Interview questions

  • How many characters does the code need? Estimate total rows, then the smallest n with 62^n above it.
  • How do you avoid two servers issuing the same ID? Pre-allocated ID ranges per server, or Snowflake-style IDs.
  • Where is the bottleneck? Reads; a cache in front of a sharded key-value store absorbs them.
  • 301 or 302? 302 if you need click analytics or editable links; 301 to shed load.

Next: Back-of-the-Envelope Estimation · Before this: Microservices and API Gateway

Common questions

How long should a short code be?

Long enough for every URL you will ever store. With 62 characters, 6 characters give about 56.8 billion codes and 7 give about 3.5 trillion. Estimate the total rows over the system's life and pick the smallest length whose 62^n covers it, with room to spare.

Should I use a counter or a hash for the short code?

A counter encoded in base62 never collides and gives the shortest codes, but needs a distributed ID source and makes codes guessable in order. Hashing the URL and taking the first characters needs no coordination, but truncated hashes collide, so every write must check and retry.

Should a URL shortener use 301 or 302?

A 301 is permanent, so browsers and caches remember it and later clicks never reach your servers — less load but no click counts. A 302 is temporary, so every click comes back, which costs more traffic but lets you count clicks, change the target or expire the link.

Which database suits a URL shortener?

The data is one large table read by its key with no joins, so a key-value or wide-column store such as DynamoDB or Cassandra fits and shards easily. A relational database with an index on the code also works at moderate scale.

How do you generate unique IDs across many servers?

Give each server a range of IDs from a coordinator such as ZooKeeper or a database sequence, so it hands them out locally until the range runs out. Snowflake-style IDs, made of a timestamp, a machine number and a sequence, are another common choice.

← Microservices and API Gateway · Back-of-the-Envelope Estimation →