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-Controlheader, 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:
- Agree on what the system must do, and how well.
- Do a little maths to find out what’s actually hard.
- Name the main pieces of data and the API.
- Start with the simplest design, and add to it each time growth breaks something.
- 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: Design a URL shortener, like bit.ly.
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: About 100 million new links a month. Each one gets opened around 100 times.
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: No. Once it’s made, it always goes to the same place.
You: Do links expire?
Interviewer: Not by default. People can set an expiry date if they want.
You: Do we need to show click counts?
Interviewer: Yes. Clicks per day, by country and by where the click came from. A few minutes of delay is fine.
You: Can people choose their own short code, like chrp.in/diwali-sale?
Interviewer: Paying users can.
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:
- A user can turn a long URL into a short link, optionally choosing their own code and an expiry date.
- Anyone who opens a short link is sent to the original page.
- 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.
- Fast: a redirect should take under about 50 ms on our side. Nobody wants to wait before the real page even starts loading.
- 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.
- Permanent: a code always points to the same URL, and a code is never handed out twice.
- Big, and mostly reads: 100 million new links a month, each opened about 100 times.
- Hard to guess: nobody should be able to try codes one by one and find links other people made.
- Click counts can be a few minutes late.
Minutes 5–10Back-of-the-envelope estimates
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: So what does all that tell 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.
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:
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
/v1/linksCreate a short link. The alias and expiry date are optional.
{ "long_url": "https://example.com/very/long/page", "alias": "diwali-sale", "expires_at": null }- 201
{ "code": "x7Kp2Qa", "short_url": "https://chrp.in/x7Kp2Qa" } - 409That alias is already taken.
/x7Kp2QaOpen a short link. The browser is sent on to the long URL.
- 302Redirect to the long URL, with
Cache-Control: private, max-age=90 - 404No link with this code.
- 410The link existed but has expired.
/v1/links/x7Kp2Qa/stats?from=2026-11-01&to=2026-11-07Click counts for one link, by day, country and source.
- 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: Why 302 and not 301?
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
httpandhttpsaddresses 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
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
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
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:
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
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.
| Requirement | Where the design handles it |
|---|---|
| Create a short link, with optional custom code and expiry | Create service; saves only if the code is free; expiry stored with the link |
| Open a short link | CDN, then redirect service, then caches, then the links store |
| See clicks by day, country and source | Click queue, then a counting job, then an analytics table |
| Fast | Three layers of caching; the database only sees rarely used links |
| Always up | Many identical servers; the database keeps copies; opening is separate from creating |
| Permanent, never reused | Saves only if the code is free; old codes are never handed out again (deep dive below) |
| Big, mostly reads | Caches carry the reads; the store spreads data across machines |
| Hard to guess | Random codes, not counting up 1, 2, 3 |
| Counts can be late | Counting happens in batches, after the redirect |
Deep diveGenerating unique codes
Interviewer: How exactly do you make the codes?
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: A cricketer with 20 million followers tweets one of your links. 300,000 clicks in the first minute. What happens?
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:
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:
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: 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: How do you stop people using it for phishing?
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: You add data centres in Europe and the US to make things faster. What breaks?
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:
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.
How one request travels through it:
- 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. - On a miss, the load balancer sends it to any redirect server. They are stateless, so you add more by number, not by design.
- The redirect server asks Redis for the code. For the hot set, this is the whole lookup: sub-millisecond.
- On a cache miss it reads the links store by primary key, checks
expires_at, fills the cache, and replies 302. - It fires a click event at the log without waiting and moves on. If the log is slow, redirects aren’t.
- 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.
- 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.
| Moment | What earns credit | What costs you |
|---|---|---|
| The first five minutes | Asking how many links are created and opened, and whether links can change | Asking 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 redirect | Explaining that Cache-Control decides how long browsers remember it, then choosing | Picking 301 because it sounds permanent |
| Making codes | Comparing approaches by how each one breaks | Cutting a hash short and not saying what happens on a clash |
| Click counts | Counting outside the redirect, then spotting the one busy partition | Adding one to a counter on every click |
| The edges | Never reusing codes, the “not found” trap across regions, scams | Reusing 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.
Stay in the loop
Get new problems and walkthroughs by email
One short email when something new is published: a deep-dive walkthrough, an LLD problem, or a real outage retold. No spam, unsubscribe any time.