System Design Road

Easy Interview walkthrough

URL shortener: the interview, played out

Almost everyone can draw a URL shortener in ten minutes. That’s exactly why it’s asked. We’ll walk through the interview the way a strong candidate runs it, and build the design up from a single server, one problem at a time.

16 min read6 interactive diagrams13 sections

This gives away every stage. For a clean attempt, practise it first, then come back.

The short version

  • Opens outnumber creates 100 to 1, so almost all your effort goes into making the redirect fast.
  • A short link never changes where it points, so you can keep copies of it anywhere without them going out of date.
  • How long a browser remembers a redirect is set by the Cache-Control header, not by whether you send 301 or 302.
  • Pick random 7-character codes and let the database reject duplicates. A clash happens about once in 300 tries.
  • The traps hide in click counting, in reusing old codes, and in users far from where a link was created.

The 45 minutes

Before we start

Think about the last time you shared a long link in a WhatsApp group. It probably got shortened along the way, to something like bit.ly/3xYz. Someone tapped it, and a fraction of a second later they were on the real page. That tiny hop is what you’re designing.

The basic version is easy: store the short code, look it up, send the user on. Most people finish that in ten minutes. The interviewer then has half an hour left, and that’s where the real interview happens. They’ll ask what breaks when a link goes viral, how you make codes, how you count clicks.

So the goal isn’t just to draw boxes. It’s to show why each box is there. We’ll go in the order that works best in a real interview:

  1. Agree on what the system must do, and how well.
  2. Do a little maths to find out what’s actually hard.
  3. Name the main pieces of data and the API.
  4. Start with the simplest design, and add to it each time growth breaks something.
  5. Go deep on the parts the interviewer pushes on.

The chat bubbles are what you’d say out loud. The text around them explains the thinking.

Minutes 0–5Requirements

Interviewer

Interviewer: Design a URL shortener, like bit.ly.

You

You: Before I draw anything, can I ask a few questions? Roughly how many links get created, and how often is each one opened?

Interviewer

Interviewer: About 100 million new links a month. Each one gets opened around 100 times.

You

You: So for every link someone creates, it gets opened about 100 times. Can a short link ever be changed to point somewhere else?

Interviewer

Interviewer: No. Once it’s made, it always goes to the same place.

You

You: Do links expire?

Interviewer

Interviewer: Not by default. People can set an expiry date if they want.

You

You: Do we need to show click counts?

Interviewer

Interviewer: Yes. Clicks per day, by country and by where the click came from. A few minutes of delay is fine.

You

You: Can people choose their own short code, like chrp.in/diwali-sale?

Interviewer

Interviewer: Paying users can.

You

You: Great. Let me write down what we’re building, and you tell me if I’ve missed anything.

Every one of those questions changes the design. That’s the test: if an answer wouldn’t change anything you draw, you didn’t need to ask it.

Functional requirements

These are the things the system does, most important first:

  1. A user can turn a long URL into a short link, optionally choosing their own code and an expiry date.
  2. Anyone who opens a short link is sent to the original page.
  3. The person who made the link can see how many clicks it got each day, by country and by source.

Things we’re not building today, said out loud so the interviewer can disagree: sign-up and login, editing where a link points, QR codes.

Non-functional requirements

These are about how well it works. Each one will shape the design later, so give it a number where you can.

  1. Fast: a redirect should take under about 50 ms on our side. Nobody wants to wait before the real page even starts loading.
  2. Always up: opening a link should work 99.99% of the time. A short link might be printed on a poster or saved in a PDF. If we’re down, all of those break. Creating links can be a little less reliable.
  3. Permanent: a code always points to the same URL, and a code is never handed out twice.
  4. Big, and mostly reads: 100 million new links a month, each opened about 100 times.
  5. Hard to guess: nobody should be able to try codes one by one and find links other people made.
  6. Click counts can be a few minutes late.

Minutes 5–10Back-of-the-envelope estimates

You

You: Let me do some quick maths. 100 million links a month is about 40 new links a second, maybe 120 at the busiest times. Opens are 100 times that, so about 4,000 a second, and up to 12,000 at peak.

You: If we keep links for ten years, that’s 12 billion links. At about 500 bytes each, around 6 terabytes.

You: For the code, I’d use letters and digits: a to z, A to Z and 0 to 9. That’s 62 characters, which is why this is called base62. Seven of them give 62 to the power 7, about 3.5 trillion possible codes. 12 billion is only a third of a percent of that.

Interviewer

Interviewer: So what does all that tell you?

You

You: Creating links is easy, 40 a second is nothing. Opening links is the real work. And because links never change, 12,000 opens a second is mostly a caching problem, not a database problem.

WhatWorkingResult
New links100 million a month ÷ 2.6 million seconds≈ 40 a second (busiest ~120)
Opens40 a second × 100 opens per link≈ 4,000 a second (busiest ~12,000)
Links kept100 million × 12 months × 10 years12 billion
Storage12 billion × 500 bytes≈ 6 TB
Possible codes62 × 62 × … seven times (62^7)≈ 3.5 trillion, we use 0.34%

Notice the last line of the conversation. Doing maths is fine, but the useful part is saying what the numbers mean for your design. “Creating links is easy” tells the interviewer where you won’t spend time.

Why does the size of the code space matter? Later we’ll pick codes at random. The chance a random code is already taken equals how full the space is. And it fills up a little more every year:

Random codes: chance a new code is already taken
62^7 = 3.5 trillion
0.0001%0.001%0.01%0.10%1.0%10%100%0y2y4y6y8y10yyears of links at 1.2B a yearchance of a collision per new code (log)6 chars7 chars8 chars0.34% at year 10

Seven characters gives 3.5 trillion codes. After ten years 0.34% are used, so a new random code collides about once in 300 tries. One conditional insert and a rare retry is the whole collision story. This is why a separate key-generation service is hard to justify at this scale.

Code length

With six characters, after ten years one new code in five would clash with an existing one. With seven, it’s one in about 300. That’s why seven is the usual choice.

Minutes 10–15Core entities and the API

Core entities

Three kinds of data carry the whole system:

  • Link: the short code, the long URL, who made it, when, and when it expires.
  • Click: which code, when, from which country and which site. A new click record is added each time, never changed.
  • User: exists, but login is out of scope today, so we only store who made each link.

Keeping clicks as separate records (instead of a “clicks” number on the link) matters later, when a link goes viral.

The API

POST/v1/links

Create a short link. The alias and expiry date are optional.

Body
{ "long_url": "https://example.com/very/long/page", "alias": "diwali-sale", "expires_at": null }
Responses
  • 201{ "code": "x7Kp2Qa", "short_url": "https://chrp.in/x7Kp2Qa" }
  • 409That alias is already taken.
GET/x7Kp2Qa

Open a short link. The browser is sent on to the long URL.

Responses
  • 302Redirect to the long URL, with Cache-Control: private, max-age=90
  • 404No link with this code.
  • 410The link existed but has expired.
GET/v1/links/x7Kp2Qa/stats?from=2026-11-01&to=2026-11-07

Click counts for one link, by day, country and source.

Responses
  • 200{ "daily": [ { "day": "2026-11-01", "clicks": 1840 } ], "countries": { "IN": 9120, "US": 640 } }

The interesting choice is the redirect. HTTP has two common ways to send a browser somewhere else: 301 means “this has moved for good”, and 302 means “go here for now”.

Interviewer

Interviewer: Why 302 and not 301?

You

You: With a 301, browsers often remember the answer and skip us next time. That sounds good, but then we never see repeat clicks, so our counts are wrong. And if we block a link for being a scam, browsers that remembered it keep going there.

You: Actually, what really decides how long a browser remembers it is the Cache-Control header we send. If I say “remember this for 90 seconds”, that’s what happens, whichever code I use. So I’d send a short Cache-Control either way, and pick 302, because we might expire or block a link later.

That second answer is the one interviewers remember. bit.ly itself sends a 301 together with “remember this for 90 seconds”, so it gets the best of both.

Two more small things worth mentioning:

  • Safe retries: if the user’s app times out and sends the create request again, we shouldn’t make two links. The client sends a unique key with each request (an idempotency key, see lesson 1.5), and we return the same link for the same key.
  • Only allow web links: accept http and https addresses only. Some other kinds of links can run code in the browser.

Minutes 15–25The design, built up

Here’s the approach: start with the simplest thing that meets the functional requirements. Then imagine the app growing. Each time something breaks, add the piece that fixes it, and say why.

Stage 1One server, one database

BrowserApp servercreate + redirectPostgreslinks tablePOST · GETsave · look upMeets every functional requirement.One machine down = every link broken.
99%
You

You: With no traffic to worry about: one app server and one Postgres database with a links table. Creating a link picks a random code and saves it. Opening a link looks up the code and sends a 302. For click counts, we save a row per click.

This already does everything on our functional list, and it’s worth saying so. A single Postgres database can handle thousands of lookups a second, so speed isn’t the first problem. Staying up is. If that one server restarts, every short link in the world stops working. That breaks our 99.99% goal on day one.

Stage 2Reads grow: stateless servers and a cache

BrowserLoad balancerNEWApp servers × NstatelessRediscode → URLNEWPostgreslinks table1. get2. on miss
79%
Green: new in this stage
You

You: Now say we’re at 4,000 opens a second, and we can’t afford to go down. First, I’d run several identical app servers behind a load balancer. They don’t keep anything in memory between requests, so any server can handle any request. If one dies, the others carry on.

You: Second, a cache. Redis keeps popular links in memory. Most opens never reach the database.

Usually caches are tricky, because when the real data changes, the cached copy goes stale. But our links never change. A cached link is never wrong. The only catch is expiry: we store the expiry date in the cache too, and never keep a cached link past it.

(Load balancers are covered in lesson 2.2, caching in lesson 2.4.)

Stage 3Global and viral: an edge, split paths, a store that partitions

BrowserEdge / CDNcaches 302s brieflyNEWLoad balancerRedirect servicestateless, manyNEWCreate serviceNEWRediscode → URL, hot setLinks storeKV, keyed by codeNEWCode generatorrandom + insert-if-absentNEWGET /x7Kp2QamissPOST1. get2. on missinsert if absent
56%
Green: new in this stage
You

You: Now users are everywhere, and sometimes one link goes viral, with thousands of clicks a second. Three changes.

You: One: a CDN. Its servers sit close to users around the world, and can answer a redirect for a minute or two without asking us. A viral link mostly gets answered there.

You: Two: separate the service that opens links from the service that creates them. Opening must never go down. If someone ships a bug in the create service, opening links shouldn’t be affected.

You: Three: at 6 terabytes, one Postgres machine gets uncomfortable. I’d move links to a key-value store that spreads data across many machines by code. Our only lookup is “find this code”, which is exactly what those stores are good at.

Try each kind of request and see where it gets answered. Very little reaches the database:

Where a redirect is answered
reply
BrowserEdge302 cached 90 sRedirect svcfills local cacheRedishot setLinks storeby codeClick log+1 event

The 302 goes back, and a click event goes to the log without waiting. If the log is slow, redirects aren't.

This request

You might notice two caches now: Redis, and a small one inside each redirect server. Why both? Every trip to Redis still crosses the network, and every request for the same link goes to the same Redis machine. A tiny in-memory copy on each server (a few thousand popular links, kept for 30 seconds) takes the load off for the hottest links. In most systems, 40 servers each keeping their own copy is a recipe for 40 different answers. Here the answer can’t change, so it’s safe.

One thing to know about databases that spread data across machines: each piece of data lives on one machine, and that machine has a limit. In DynamoDB, a single partition handles about 3,000 reads a second. A viral link sending 12,000 reads straight to the database would get throttled, no matter how big the whole table is. That’s why the caches in front aren’t optional.

Stage 4Analytics without slowing redirects

BrowserEdge / CDNcaches 302s brieflyLoad balancerRedirect servicestateless, manyCreate serviceRediscode → URL, hot setLinks storeKV, keyed by codeCode generatorrandom + insert-if-absentClick logKafkaNEWRollup jobper-minute batchesNEWAnalytics DBclicks_dailyNEWGET /x7Kp2QamissPOST1. get2. on missinsert if absentclick eventupsert counts
46%
Green: new in this stage
You

You: Last, click counts. If every redirect writes a row to the database before replying, the user waits for our counting. So instead, the redirect server drops a small “someone clicked” message into a queue and replies right away. A separate job reads the queue in batches and adds up counts per minute and per day.

You: If the counting side is slow, redirects aren’t. And the interviewer said counts could be a few minutes late, which is exactly what this gives us.

(Queues like Kafka are covered in lesson 4.1.)

Checking the design against the requirements

Before going deeper, walk back through your list. It takes thirty seconds and catches gaps before the interviewer does.

RequirementWhere the design handles it
Create a short link, with optional custom code and expiryCreate service; saves only if the code is free; expiry stored with the link
Open a short linkCDN, then redirect service, then caches, then the links store
See clicks by day, country and sourceClick queue, then a counting job, then an analytics table
FastThree layers of caching; the database only sees rarely used links
Always upMany identical servers; the database keeps copies; opening is separate from creating
Permanent, never reusedSaves only if the code is free; old codes are never handed out again (deep dive below)
Big, mostly readsCaches carry the reads; the store spreads data across machines
Hard to guessRandom codes, not counting up 1, 2, 3
Counts can be lateCounting happens in batches, after the redirect

Deep diveGenerating unique codes

Interviewer

Interviewer: How exactly do you make the codes?

You

You: There are three common ways. Each one fails differently, so let me compare them.

Bad solutionHash the URL and cut it short

Run the long URL through a hash function (like MD5) and keep the first seven characters. It sounds neat, because the same URL always gets the same code. But nobody asked for that. And because you cut the hash short, two different URLs can still end up with the same seven characters. So you need a “check and try again” step anyway, and the hash didn’t really buy you anything.

Good solutionA counter

Count up: link 1, link 2, link 3, and write each number in base62. Codes never clash, and they stay short. But think about what that reveals. If your link is x7Kp2Qa, anyone can try x7Kp2Qb, x7Kp2Qc and so on, and find other people’s links. People shorten links to private documents and unlisted videos, assuming nobody can find them. Researchers have done exactly this kind of scan on real shorteners and found private files. A counter also tells competitors how many links you make: take two codes a week apart and subtract.

Great solutionRandom codes, and let the database say no

Pick seven random characters, then save the link with an instruction that only works if the code isn’t taken yet. In Postgres that’s INSERT … ON CONFLICT DO NOTHING. If the code was taken, pick another one and try again. With 0.34% of codes used, that happens about once in 300 tries. Nobody needs to coordinate anything: the database’s uniqueness check is the referee. Custom codes go through the same check, so they can never clash with random ones.

If your company really needs counters (some systems do), you can still hide the order. Run each number through a reversible scramble with a secret key, so 1, 2, 3 come out looking random but never repeat. The general idea is called format-preserving encryption, and it’s a nice thing to mention if the interviewer pushes.

Deep diveThe viral link

Interviewer

Interviewer: A cricketer with 20 million followers tweets one of your links. 300,000 clicks in the first minute. What happens?

You

You: That’s about 5,000 clicks a second on one link. Once the CDN has it, most clicks get answered there. Whatever gets through hits our redirect servers, and each one keeps it in its small local cache after the first request.

You: The risky moment is the very first second, before anything is cached. Thousands of requests arrive at once, all miss the cache, and all go to the database for the same link. That’s called a cache stampede. The fix: when many requests miss on the same link at the same moment, let only one of them go to the database, and have the others wait for that answer.

Lesson 2.4 shows a stampede and its fixes:

A popular entry expires at 9:04 pm
database queries: 930
~2,000 reads/sprofile:commentatorcachedCacheDatabaserunning: 0CPU

930 copies of the same query hit the database in a few seconds, and everything else on Chirp slowed down with it.

Protection

Deep diveCounting 5,000 clicks a second

Bad solutionAdd one to a counter on every click

UPDATE links SET clicks = clicks + 1 on every redirect. For a viral link, every click has to wait its turn to update the same row, and the user is waiting too. Lesson 4.5 shows how quickly that queue grows.

Good solutionSend click events to a queue

Drop a message per click into Kafka and count later. This is the right idea, with a hidden trap. Kafka splits a topic into partitions, and keeps messages in order within a partition. The obvious choice is to group messages by short code. But then one viral link sends all its clicks to one partition, which only one worker reads:

One viral link, 5,000 clicks a second
10 min
Redirect servers× 40Partition 0200/sPartition 1backlog growingPartition 2180/sRolluplag: 9 minutesclicks_daily

The consumer for partition 1 falls behind. "A few minutes of delay" becomes nine, and every other link that happens to hash to partition 1 is late too. Adding partitions doesn't help: the hot key still maps to exactly one.

Partition click events by

Great solutionCount a little first, then send

Each redirect server keeps a small tally in memory (“x7Kp2Qa: 212 clicks this second”) and sends one summary per second, to a random partition. The counting job adds the summaries up. Forty servers send at most forty messages a second, however viral a link gets. We lose the order of individual clicks, but counting never needed it.

Three more things worth knowing about click data:

  • Link previews aren’t people. When you paste a link into WhatsApp, Slack or iMessage, their servers open it to build the preview card. Filter those out, or your counts are inflated.
  • Messages can arrive twice. If a counting job crashes halfway, it may process the same batch again. Design it so counting a batch twice gives the same result, or accept a small, known error.
  • Unique visitors don’t need a list of everyone. A HyperLogLog estimates “how many different people” in about 12 KB per link, with under 1% error. Lesson 4.5 explains how.

Deep diveExpiry, deletion, and never reusing codes

You

You: Every time a link is opened, we check its expiry date, even when it comes from a cache. Caches never keep a link past its expiry, and the CDN only keeps redirects for a minute or two. So an expired link stops working within a couple of minutes.

You: And we never give an old code to a new link. Imagine a code printed on thousands of posters. If it later points somewhere new, all those posters now send people to a stranger’s page, maybe a scam. So dead codes stay reserved forever. We can delete the long URL to save space, but the code itself is never reused.

Real-world reminder: in 2024 Google announced that all goo.gl links would stop working in 2025. People pushed back hard, because those links were inside documents, videos and posts that nobody could edit any more. Google later kept the links that were still being used. A short link is a promise that lasts longer than you’d think.

Deep diveStopping scams

Interviewer

Interviewer: How do you stop people using it for phishing?

You

You: A few layers. Limit how many links one account or one IP address can create, because most abuse is about volume. Check new links against a list of known bad sites, and check again later, since a page can turn bad after it’s shared. Be able to block a link quickly, which is another reason redirects are only cached briefly. And offer a preview page that shows where a link goes before you’re sent there.

You: Also, run short links on their own domain. Scammers use every shortener, so shortener domains sometimes get blocked by email filters and browsers. You don’t want that to take your main website down with it.

That last point usually comes from people who’ve actually run one of these. It’s one reason X uses t.co instead of a path on x.com.

Deep diveUsers all over the world

Interviewer

Interviewer: You add data centres in Europe and the US to make things faster. What breaks?

You

You: Opening links is a read, so every region can have its own copy of the links data and its own cache. New links are saved in the creator’s home region first, then copied to the others. The tricky moment is right after someone creates a link.

Here’s the story. Someone in Mumbai creates a link and shares it in a group chat. A friend in Frankfurt taps it 200 milliseconds later, before the link has been copied to Europe. Try both options:

Created in Mumbai, clicked in Frankfurt 200 ms later
30 s
replicationCreatorMumbaiMumbai storex7Kp2Qa createdFrankfurt storex7Kp2Qa arrivesFrankfurt redirectcaches 404 for 60 sFriendFrankfurt

Every click in Europe still gets 404 for the next minute, from the cache. A 200 ms replication gap has become a 60-second outage for this link, and only for people in one region, which is the hardest kind of bug report to reproduce.

On a local miss

Remembering “this code doesn’t exist” seems smart, because bots try random codes all day. But combined with the copying delay, a 200 ms gap turns into a full minute where the link is broken for everyone in Europe. Two fixes: never remember “not found” for more than a second or two, and if a code is missing locally, ask its home region before saying 404. You can even hide the home region inside the code itself, using a tiny bit of it, so the redirect server knows where to ask.

(Replication lag, the delay before copies catch up, is covered in lesson 2.5.)

The complete architecture

Here’s everything together: the full design from the last stage, with every part we added along the way. If you only remember one picture from this question, make it this one.

BrowserEdge / CDNcaches 302s brieflyLoad balancerRedirect servicestateless, manyCreate serviceRediscode → URL, hot setLinks storeKV, keyed by codeCode generatorrandom + insert-if-absentClick logKafkaRollup jobper-minute batchesAnalytics DBclicks_dailyGET /x7Kp2QamissPOST1. get2. on missinsert if absentclick eventupsert counts
46%

How one request travels through it:

  1. A browser opens chrp.in/x7Kp2Qa. If the edge has a cached 302 for it (popular links, a few minutes’ max-age), it answers without touching us.
  2. On a miss, the load balancer sends it to any redirect server. They are stateless, so you add more by number, not by design.
  3. The redirect server asks Redis for the code. For the hot set, this is the whole lookup: sub-millisecond.
  4. On a cache miss it reads the links store by primary key, checks expires_at, fills the cache, and replies 302.
  5. It fires a click event at the log without waiting and moves on. If the log is slow, redirects aren’t.
  6. A rollup job reads the log in batches and upserts per-minute and per-day counts. A link clicked a million times is a million events, but only one write per minute to the analytics table.
  7. Creates take the other branch: generate a random code, insert it only if absent, retry on the rare conflict, and return it.

How this answer gets scored

Interviewers don’t tick boxes, but their notes on this question tend to land on the same few moments. Here’s what each one sounds like from the other side of the table.

MomentWhat earns creditWhat costs you
The first five minutesAsking how many links are created and opened, and whether links can changeAsking about the tech stack, or starting to draw
The numbers“Creating links is easy, so I won’t spend time on it”Ten minutes spreading a database that gets 40 writes a second
The redirectExplaining that Cache-Control decides how long browsers remember it, then choosingPicking 301 because it sounds permanent
Making codesComparing approaches by how each one breaksCutting a hash short and not saying what happens on a clash
Click countsCounting outside the redirect, then spotting the one busy partitionAdding one to a counter on every click
The edgesNever reusing codes, the “not found” trap across regions, scamsReusing expired codes to “save space”

What the left column has in common: every choice points back to a requirement or a number from earlier. If you can say where each box came from, the interviewer will notice.