One hundred million long URLs arrive every day and each must shrink to seven characters, survive ten years, and redirect in milliseconds. This lesson builds the shortener: count the load, pick the redirect, then mint codes that never collide.
Outcomes
The interview opens with four questions and the answers set every later decision. Example mapping: a long systeminterview.com URL with query parameters becomes https://tinyurl.com/y7keocwj, and clicking the alias redirects to the original. Traffic is 100 million new URLs per day. Codes must be as short as possible over 62 characters (digits plus both cases). Shortened URLs are never deleted or updated, which removes a whole class of invalidation logic. Two use cases remain: shorten a long URL, and redirect a short URL.
Run the envelope live: 100M divided by 86,400 seconds is about 1,160 writes per second. Reads outnumber writes 10 to 1, so plan for 11,600 reads per second. Ten years of writes is 100M times 365 times 10, or 365 billion rows. At 100 bytes per URL that is 36.5 terabytes per year, 365 TB for the decade. Reads dominate, so the design that wins is the one that serves redirects cheapest.
Two endpoints carry the whole API, REST style. POST api/v1/data/shorten takes the long URL and returns the short URL. GET api/v1/shortUrl returns the long URL as an HTTP redirect. The redirect has two flavors and the choice is the first real tradeoff of the lesson.
Rule of thumb: pick 301 when the priority is shedding server load, 302 when the priority is knowing who clicked. Most interviewers follow up by asking what changes when analytics is added later, and the answer is that flipping 301 to 302 replays load onto the web tier, so the cache and database must already be sized for it.
The naive redirect store is a hash table of short URL to long URL pairs. It fits the whiteboard and fails reality: memory is limited and expensive, so the mapping lives in a relational table with three columns (id auto increment, shortURL, longURL). Reads outnumber writes ten to one, so a cache sits in front of the database and the redirect path checks it first. The load balancer sprays clicks across stateless web servers, each of which reads through the same cache.
How to read: Follow the dots: the click fans out across web servers, takes the upper path on a cache hit or drops to the database on a miss, and lands on the long URL.
You click the short URL link.
The load balancer forwards to one web server.
Cache hit: return the long URL directly.
Cache miss: fetch from the database, then return.
Missing in both: the short URL is invalid.
The browser follows the redirect to the long URL.
The short URL is the domain plus a code, so the design problem reduces to a function from long URL to code that is reversible: every long URL maps to one code, and every code maps back. The alphabet holds 62 characters, and the smallest length covering 365 billion URLs comes from 62 to the 7, which is about 3.5 trillion. Length 6 holds only 56.8 billion, so 7 is the floor, not a guess.
The shortening flow is six steps and the database check up front is what makes repeat submissions free. Given a long URL, look it up: if present, return the stored code untouched. If new, ask the unique ID generator for an ID, convert it to base 62, and insert the (ID, code, long URL) row. Concrete trace from the chapter: the Wikipedia systems design URL arrives, the generator returns 2009215674938, conversion yields zn9edcu, and that triple is stored.
How to read: Follow the dots: the submission checks the database first, takes the upper path for a known URL or mints and converts on the lower path, and a row lands in storage.
Submit the long URL to the web server.
Long URL already stored: return its code.
New URL: request a unique ID from the generator.
Convert the ID to base 62 for the code.
Insert the ID, code, and long URL row.
Return the short URL to the client.
The distributed ID generator is doing quiet heavy lifting here: globally unique IDs without coordination per request, which is exactly what Chapter 7 built the snowflake for. If the generator stalls, shortening stalls, so it inherits the mission-critical availability requirement.
The lines that matter are the digit map and the divide loop. Everything else is argument plumbing. Run it: 11157 encodes to 2TX, and decoding reverses the walk.
ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
BASE = len(ALPHABET) # 62
def encode(num):
assert num >= 0
if num == 0:
return ALPHABET[0]
out = ''
while num > 0: # repeated divmod, least significant digit first
num, rem = divmod(num, BASE)
out = ALPHABET[rem] + out
return out
def decode(s):
num = 0
for ch in s: # Horner walk: shift left by one digit, add value
num = num * BASE + ALPHABET.index(ch)
return num
print(encode(11157)) # 2TX, the chapter trace
print(decode('2TX')) # 11157, round trip holds
print(encode(2009215674938)) # chapter-scale ID to 7-char code
print(len(encode(62**7 - 1))) # 7: the full range fits the picked lengthWrap-up talking points when time remains: a rate limiter in front of shortening (malicious clients can flood minting, see Chapter 4), stateless web tier scaling by adding servers, database replication plus sharding, an analytics pipeline for click counts, and the availability versus consistency stance inherited from Chapter 1. The interview closes the same arc it opened: scope with numbers, high-level flows, deep dive on the code mint, then honest extras.