A short link is seven characters of text. Someone drops it into a WhatsApp group, the
group forwards it twice, and by dinner half a million people have opened it. Behind those
seven characters is a service that does exactly one thing: take a code, find a URL, send
the browser there.
That is why it opens so many interviews. There is nowhere to hide. Get the code wrong and
two people walk away with the same link. Get the read path wrong and your database falls
over at a tenth of the traffic you just promised to handle.
The order below is the order to use in the room. Ask, count, draw, then defend.
The worst opening move is drawing a box. Spend the first three minutes asking questions
instead, because every answer moves a line in the design you are about to draw.
Here are the six that earn their time, with the replies you will usually get and what
each one settles.
You ask
They say
What it settles
How many links a day, and how many clicks per link?
About 100 million new links a day, and reads run roughly 100 times writes.
Everything else on this page. At 100 to 1 you are building a cache with a database attached, not a database with a cache bolted on.
How short does the link have to be?
As short as you can make it while staying unique for years.
Seven characters, and you can prove it in one line of arithmetic instead of guessing. That comes two blocks down.
Can people pick their own code, like ig.ee/holi-sale?
Yes, an optional custom alias, but only a small slice of links use it.
A second path through the create flow, and the only place where a create is allowed to fail with a conflict. Everything else stays collision free by construction.
Do links expire, and can they be deleted?
Five years by default, and the owner can delete one early.
An expiry column, a nightly sweeper, and a hard rule that a retired code is never handed out again. Reusing a code sends old traffic to a new destination, which is how a shortener turns into a phishing tool.
Do we need click analytics, and how fresh?
Counts and rough geography. A minute behind is fine.
Clicks go to an event log, never into the redirect request. A minute of allowed lag is permission to batch, and batching is what keeps the redirect fast.
One region or the whole world?
India and the US to start.
The redirect tier runs in both regions. Creates stay in one, because 1,200 writes a second do not need a second home and multi region writes would cost you a week of the interview you do not have.
Not one of those questions was about technology, and that is the point. You now know the
read ratio, the code length, the failure mode that matters, and where the servers live.
Drag these. The point is not the default answer, it is watching which decision each input
controls.
Napkin math
100 million
100 to 1
5 years
Writes100M / 86,400s = 1,157 per second
Reads1,157 x 100 = 115,741 per second
Links stored100M x 365 x 5 = 182.5 billion
Storage at 500 bytes a row91 TB
Shortest base62 code with 10x headroom62^7 = 7 characters
115,741 reads/sec
the number that decides every other choice on this page
Two things fall out of this. A 7 character code is enough, so stop worrying about running out. And at 100 to 1 you are reading far more than you write, which decides the shape of the whole design.
The number people get wrong
Almost everyone computes storage and stops there. Storage is the boring answer. A few
dozen terabytes is a solved problem you can buy with a credit card. The number that
matters is the read rate, because it is what puts the cache, the read replica and a
fat stateless redirect tier into the picture.
Why: POST and not PUT, because the server picks the code. Send an Idempotency-Key header with it. A client that times out and retries should get the same code back, not burn a second one and leave an orphan row behind. If customAlias is set, this is the one request that can come back 409.
GET/{code}
returns 302 Location: https://indgeek.com/...
Why: 302 and not 301. A 301 gets cached by the browser more or less forever, so you stop seeing the clicks and you can never revoke the link. The extra round trip is the product. An unknown or expired code returns 404 from the same cheap lookup, never a database scan.
Why: Metadata lives on a different path from the redirect, so the hot path stays one lookup with no joins and no auth check. Different path also means different rate limits.
DELETE/api/v1/urls/{code}
returns 204
Why: Soft delete. The row stays, a flag flips, and the cache entry is dropped. Hard deleting frees the code for reuse, and a reused code points old traffic at a new destination.
A custom alias needs no extra column. It is just a code that a human chose instead of the generator, so the primary key does the uniqueness check for you and returns a 409 when it fails.
There is no auto increment id here on purpose. It would cost 8 bytes across 180 billion
rows and buy you nothing, because nothing ever looks a row up by it.
Clicks do not belong in this table either. One row per click turns a system doing about
1,200 writes a second into one doing 116,000, and it drops a write into the path of every
redirect. Clicks get their own pipeline, further down.
Figure 1 is the design, at the level of detail you would actually draw in 45 minutes. Two
paths share a front door and then have almost nothing in common. Creates run across the
top and are allowed to be slow. Redirects run across the bottom and are not.
1POST /urls2give me 1,00031 write/sec4insert5GET /kY7pQ2m6hit, 99%7miss, 1%8click event910roll up11expired rows12Clientbrowser, appLoad balancerTLS, rate limitCreate servicestateless, x3ID range serviceblocks of 1,000Counter storeone row, one lockURL databaseprimary + replicaExpiry sweepernightly jobRedirect servicestateless, x8Redis clustercode to URL, 1 dayEvent logclicks, keyed by codeAggregator1 minute windowsAnalytics storecolumnar
Figure 1. Everything above the load balancer row is the write path, about 1,200 requests a second. Everything below it is the read path, about 116,000. They share nothing but the front door and one database.
Walking Figure 1 in order:
Every request enters at the load balancer, which terminates TLS and rate limits by
caller. Creates are the expensive request, so the limit lives here rather than in your
application code.
A create goes to the create tier. It is stateless and small: 1,200 requests a second
across three hosts is nothing.
The host needs an id and does not ask the database for one. It takes the next id from a
block of 1,000 it reserved earlier. That block is the subject of the first deep dive.
The ID range service is the only thing touching the counter row, and it touches it
about once a second for the whole fleet.
The create writes the row, then writes the same entry straight into Redis so the very
first click is already warm.
A redirect goes to the redirect tier, which is stateless and much bigger. Nothing here
remembers anything, so you scale it by adding hosts and you deploy a copy of it in
every region you serve.
The host reads Redis. About 99 requests in 100 end here, in roughly a millisecond, and
the user gets a 302.
On a miss it reads the database, backfills Redis, and returns. A miss is the only time
this system touches a disk. If the code does not exist at all, that is a 404 from the
same lookup.
The click is published to an event log and the response goes out immediately. Nothing
downstream can make a user wait.
The aggregator rolls raw events into one minute windows.
Those windows land in a columnar store, which is what a dashboard would read.
A nightly sweeper clears rows past their expiry. The live path never scans for expired
links, it just stops finding them.
Notice what is missing. No queue in the redirect path. No service mesh. No search index.
Every box in Figure 1 is there because a number from the napkin math put it there, and you
should be able to name that number for each one.
Boxes and arrows show you what exists. They do not show you who waits for whom, and that is
the thing an interviewer is listening for. Figure 2 is the create path in the order it
actually happens. Press play, or step through it.
ClientCreate serviceID range serviceURL databaseRedisPOST /urls + Idempotency-Keynext id from local blockrefill, once per 1,000scramble, then base62insert rowwrite code to URL201 { shortUrl }
1/7 The idempotency key is the client saying "if you already did this, do not do it twice". A retry after a timeout gets the first code back instead of burning a second one.
Figure 2. Creating a link touches four systems and waits for two of them. The id comes out of memory, so the common create never asks another service for anything.
Your answer
Before the deep dives: where does the short code come from, and what breaks the moment you run a second copy of the service that creates them?
This is the part interviewers push on, and the part most answers get wrong.
The tempting approach is to hash the long URL, take the first seven base62 characters and
call it done. It fails twice over. Two people shortening the same URL usually want two
different links, because they want separate click counts. And truncated hashes collide: in
62^7 slots a collision becomes likely somewhere around a few million links, which you will
pass on day one. You can check the table and retry, but now every create is a read plus a
write, and the retry rate climbs as the table fills.
Random codes have the same problem in a nicer suit. Fine while the table is empty, quietly
worse forever after.
The approach that holds up never looks at the table at all. Figure 3 steps through it.
reserve 1,000Counterone row, one lockHost block8,000,000 to 8,000,999Scrambleid x K mod 62^7kY7pQ2mbase62, 7 chars
reserve 1,000Counterone row, one lockHost block8,000,000 to 8,000,999Scrambleid x K mod 62^7kY7pQ2mbase62, 7 chars
1/4 A create host asks for a block of 1,000 ids and gets one. At 1,200 creates a second across the fleet that is about one write a second to a single row, which any database will do in its sleep.
Figure 3. The counter is touched about once a second by the entire fleet. Everything after that happens in memory on one host, with no network call and no lock.
Be honest about what the scramble buys you. It hides the sequence from a casual observer,
and anyone who collects a handful of codes and does some algebra can reverse it. If the
requirement is that links must be unguessable by a motivated attacker, you need real random
bits and the collision check that comes with them. Say that trade out loud rather than
letting the interviewer find it.
Custom aliases skip this machinery entirely. The user supplies the code, you insert it, and
the primary key either accepts it or throws a duplicate key error you turn into a 409. No
lookup first, because a lookup then an insert is a race between two users typing the same
alias at the same moment.
The follow up you will get
"What happens when the counter service is down?" Every create host is holding up to 1,000
ids, so creates carry on for about a second per host of buffered capacity. Raise the block
size to 100,000 and an outage of several minutes goes unnoticed. The cost is that you burn
through the id space faster after every restart, and 62^7 leaves you plenty of room to
waste.
The redirect is a cache lookup and little else. Cache aside, a one day TTL, and the entry
written into Redis at create time so even the first click is a hit. At a 99% hit ratio the
database sees about 1,200 reads a second, which one primary and one replica handle without
noticing.
Figure 4 is one click, end to end. The last two messages are the ones people forget, and
they are the whole argument for a 302.
BrowserRedirect serviceRedisURL databaseEvent logGET /kY7pQ2mGET url:kY7pQ2mhit, about 1msmiss: read the rowbackfillclick event, fire and forget302 Location: indgeek.com/...follows the Location header
1/8 Any host will do. The redirect tier holds no state, so the load balancer can send this anywhere, including a host in another region.
Figure 4. One click. The cache answers 99 times in 100, the click event is fired without anybody waiting for it, and the browser makes a second request of its own to reach the target.
All of that holds right up until a single link goes viral. Now the traffic is not spread across
millions of keys. It is 400,000 requests a second for one key, and one key lives on exactly
one Redis shard. Adding shards does not help. The shard that owns the key sits at 100% CPU
while the other eleven idle.
The fix is unglamorous and it works: cache the hottest entries in the memory of the redirect
process itself, with a TTL of one second. A viral link is then served out of a local hash
map. One second of staleness on a link created hours ago is not a problem anybody has, and
Redis drops from 400,000 requests a second on that key to one per host per second.
You will watch that break, and then get repaired, under Break it below.
Every redirect wants to record an event. Do it synchronously and the user waits on a write
before they get their 302, and a clicks table taking 116,000 writes a second sits directly
in front of your most important request.
So do not do it synchronously.
fire and forget12roll up3Redirect serviceEvent logKafka, keyed by codeAggregator1 minute windowsAnalytics storecolumnar
Figure 5. The redirect returns the moment the event is handed to the local producer buffer. Nothing downstream of that arrow can slow a user down.
The trade you are making: if a redirect host dies with events still in its buffer, those
clicks are gone. For a click counter, losing a handful in a crash is fine, and saying so is
a stronger answer than pretending durability is free. If the question were payments instead
of clicks, the answer flips, and the write goes into the request path where it belongs.
Drag the traffic up. Watch which box goes first, and read why it was that one.
Redirect traffic
5k/s
5k/s60k/s120k/s250k/s400k/s
on missClientsRedirect APIstatelessRedis12 shardsDatabaseprimary + replica
on missClientsRedirect APIstatelessRedis12 shardsDatabaseprimary + replica
Healthy. Eight redirect hosts, one Redis cluster, one primary with a replica. Nothing here is interesting, which is the whole point of the design.
Read every step as text
5k/s · Healthy. Eight redirect hosts, one Redis cluster, one primary with a replica. Nothing here is interesting, which is the whole point of the design.
60k/s · Healthy. At a 99% hit ratio the database is seeing about 600 reads a second. It is the least busy thing in the picture and it will stay that way.
120k/s · Degraded. The redirect tier saturates first, on CPU spent parsing requests and writing responses. This is the good kind of bottleneck: the tier holds no state, so you add hosts and the problem goes away.
250k/s · Degraded. Spread evenly across millions of links, Redis is fine on throughput but the p99 starts drifting as one or two shards run hotter than the rest. Rebalance, or add shards.
400k/s · Down. Now all of it is one viral link. One key, one shard, 100% CPU on that shard while the other eleven idle. Sharding cannot fix this, because the unit of sharding is the key. The database, meanwhile, is bored.
The repair takes one box and about forty lines of code.
1hit, 99.99%2miss34ClientsRedirect APIstatelessProcess cache1 second TTLRedis12 shardsDatabaseprimary + replica
Figure 6. A one second local cache in front of Redis. The viral key is answered out of process memory, and Redis sees one request per host per second for it instead of 400,000.
Where this shows up outside interviews
The hot key problem is not a URL shortener quirk. It is the same shape as one product page
during a sale, one match in a live scores feed, and one celebrity account in a social
graph. The answer is nearly always the same: move the hottest slice one layer closer to
the request and accept a small window of staleness.
Take me through what happens when someone clicks a short link.
Opening move. They want to know if you go to the read path first or waste ten minutes on the create path.
You
It hits the load balancer, goes to any redirect host since they hold no state, and that host looks the code up in Redis. Roughly 99 times out of 100 it is there and we return a 302. On a miss we read the row, put it back in Redis, and return. No writes, no joins, one network hop on the happy path.
Names the hit ratio and the shape of the miss without being asked. That is the difference between describing a diagram and understanding one.
Interviewer
Why 302 and not 301?
A test. There is a right answer and it is not about performance.
You
A 301 gets cached by the browser more or less permanently, so we stop seeing the clicks and we can never revoke or retarget the link. Both of those are product features here, so we pay for the extra round trip.
Trade stated as a choice with a cost. Short.
Interviewer
One link is getting 400,000 requests a second. What happens?
The real question of the round. Everything before this was warm up.
You
Redis shards by key, so all of that lands on one shard and that shard runs out of CPU while the rest sit idle. Adding shards does not help because the key cannot be split. I would put a small in-process cache on each redirect host with a one second TTL. The link is immutable in practice, so a second of staleness costs nothing, and Redis drops from 400,000 requests a second on that key to about eight.
Diagnoses before prescribing, and explains why the obvious fix does not work. Says the number the fix produces.
Interviewer
How would you pick that TTL?
Open ended on purpose. They are checking whether you invent a confident number.
You
I would not pick it from first principles. I would start at one second because it is obviously safe for immutable data, then look at how much Redis traffic is left and whether anyone complains about delete latency. If deletes need to be instant I would add a small invalidation broadcast rather than shrinking the TTL, since that scales better. I have not run this at that size, so I would want the numbers before committing.
Admitting the limit of your experience while still giving a method is a scoreable answer. Guessing here is not.
1. At 100 million new links a day kept for five years, why is a six character code not enough?
2. Why multiply the counter value by a constant modulo 62^7 instead of generating a random code?
3. Same system, but a partner starts bulk importing and writes now outnumber reads ten to one. What is the first thing that changes?
Worth memorising
1,200 writes a second, 116,000 reads. The ratio decides the design, not the volume.
62^6 is 57 billion, 62^7 is 3.5 trillion. That gap is the whole argument for seven characters.
90TB of rows. Storage is the boring number here. Say it and move on.
One viral link is 400,000 requests a second on one shard. No amount of sharding splits a single key.
Say this in 60 seconds
A URL shortener is a read heavy system, about a hundred to one, so I would design it as a cache with a database attached. Creates go to a small stateless tier that gets ids from a counter service in blocks of a thousand, scrambles each id with a modular multiply so the codes are not walkable, and base62 encodes it to seven characters. That means no collision checks and one write per create. Custom aliases skip the generator and let the primary key reject duplicates. Redirects go to a large stateless tier that reads Redis and returns a 302, falling back to the database on a miss and backfilling. Clicks are published to an event log and aggregated offline, never in the request path. The failure mode I would call out is a single viral link, since it lands on one Redis shard and no amount of sharding helps, and I would fix that with a one second in-process cache on each redirect host.