"Design a URL shortener, like TinyURL or Bitly."
It sounds like a five-minute idea — take a long URL, hand back a short one. But sit with it for a moment and questions pile up: how do you generate a code that's short and unique? What happens when two users' codes collide? Does the code need to be unguessable? What happens at a crore requests a day?
Let's work through this the way the last eight lessons taught you to — requirements first, classes second, patterns only where they earn their place.
Clarifying questions
Ask questions that would actually change the design — not filler questions:
- Should codes be sequential/predictable, or random and hard to guess?
- Do we need custom aliases (a user picks their own short code)?
- Do links expire?
- Roughly what scale — thousands a day, or millions?
- Do we need click analytics, or just redirect?
For this lesson, assume: random-looking codes are fine, no custom aliases for now, no expiry yet (we'll add it as an extension), and scale matters enough to think about collisions and distribution.
What objects do we need?
Reading the requirement ("shorten a URL, and later expand a code back to it") the way Lesson 1 taught — nouns first, then ask what job each one owns:
- URLShortener — the core service:
shorten(longUrl)andexpand(code). - LinkGenerator — owns exactly one job: turn something into a short code.
- URLRepository — owns storing and looking up the (code → long URL) mapping. This is a data-access layer, kept separate from business logic (the "service → repository" layering mentioned back in Lesson 1's interview cues).
Generating short codes — more than one reasonable way
Here's the exact Strategy signal from Lesson 5: there is more than one reasonable way to generate a code, and the choice may need to change later (custom aliases, a premium tier, a different scheme entirely).
class LinkGenerator:
def generate(self, long_url: str) -> str:
raise NotImplementedError
BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
class CounterGenerator(LinkGenerator):
def __init__(self):
self._counter = 0
def generate(self, long_url: str) -> str:
self._counter += 1
return self._to_base62(self._counter)
def _to_base62(self, num: int) -> str:
if num == 0:
return BASE62[0]
digits = []
while num:
digits.append(BASE62[num % 62])
num //= 62
return "".join(reversed(digits))
class HashGenerator(LinkGenerator):
def generate(self, long_url: str) -> str:
import hashlib
digest = hashlib.md5(long_url.encode()).hexdigest()
return digest[:7] # truncated — must be checked for collisionsCounterGenerator (an incrementing number, base-62 encoded) gives short, guaranteed-unique codes with no collision checking needed — but codes are predictable and guessable in order. HashGenerator (hash the URL, truncate it) gives less-predictable codes, but now two different URLs can hash to the same short string, so the repository must detect and handle that collision (retry with a salt, or extend the code). Neither is "the correct one" — say the trade-off out loud and pick based on the requirements you clarified.
Putting it together
class URLRepository:
def __init__(self):
self._store = {} # code -> long_url (a real system: a database)
def save(self, code: str, long_url: str):
self._store[code] = long_url
def get(self, code: str) -> str | None:
return self._store.get(code)
def exists(self, code: str) -> bool:
return code in self._store
class URLShortener:
def __init__(self, generator: LinkGenerator, repo: URLRepository):
self.generator = generator # depends on the abstraction (DIP)
self.repo = repo
def shorten(self, long_url: str) -> str:
code = self.generator.generate(long_url)
while self.repo.exists(code): # collision handling
code = self.generator.generate(long_url + code)
self.repo.save(code, long_url)
return code
def expand(self, code: str) -> str | None:
return self.repo.get(code)Notice URLShortener depends on the LinkGenerator abstraction, not a concrete class — exactly Dependency Inversion, so swapping CounterGenerator for HashGenerator means changing one line where URLShortener is constructed, nothing inside it.
Sequence: shorten and expand
Scaling: collisions and distribution
Two scale concerns worth naming, even without a full HLD discussion:
- Collisions: the
while self.repo.exists(code)retry loop above handles them functionally, but at very high volume you'd rather avoid collisions altogether — which is exactly whyCounterGeneratoris often preferred at scale. - Distributed counters: if
URLShortenerruns on many servers, a naive in-process counter on each server can generate duplicate codes. A common fix is pre-allocating counter ranges to each server (e.g. server A gets 1–1,000,000, server B gets 1,000,001–2,000,000), or using a centralized ID-generation service.
What if the interviewer changes this?
expiresAt field to the stored mapping in URLRepository, and check it inside expand() before returning a URL. LinkGenerator is untouched — a good sign the responsibilities were split correctly.shortenWithAlias(longUrl, alias) method that skips LinkGenerator and calls repo.exists(alias) directly, returning an error if taken. The generation strategy stays untouched — this is purely an alternate entry point into the same repository.Common mistakes
- Hard-coding one generation scheme directly inside `URLShortener` instead of behind a `LinkGenerator` interface.
- Forgetting to handle hash collisions when using a hash-based generator.
- Ignoring the distributed-counter problem when asked about scale.
- Mixing storage/lookup logic directly into `URLShortener` instead of a separate `URLRepository`.
Quick recap
| Class | Job |
|---|---|
| URLShortener | Core service: shorten() and expand(), coordinates the other two. |
| LinkGenerator (Strategy) | How a code is generated — counter-based or hash-based, swappable. |
| URLRepository | Stores and looks up the code-to-URL mapping. |
One thing to remember
"How do I generate the code" and "how do I store the mapping" are two different jobs — the moment you notice more than one reasonable way to do the first, that's a Strategy, not a hard-coded function.
Practice Zone
Five MCQs, then two reasoning questions.
Which clarifying question actually changes the design of a URL shortener, and is worth asking first?
Asked in

Why is base-62 encoding of an incrementing counter a common choice for generating short codes?
Asked in


If you hash the long URL (e.g. MD5) and take the first 7 characters as the short code instead of using a counter, what new problem do you introduce?
Asked in

Why does a URL shortener's code-generation logic naturally fit the Strategy pattern from Lesson 5?
Asked in

At very high scale (millions of shortens per day, distributed servers), what breaks about a single in-process incrementing counter?
Asked in


Think it through, then reveal:
Why should URLShortener depend on a LinkGenerator interface rather than directly calling a generate_code() function inside itself?
Asked in

The interviewer says: "Now add expiring links — a short URL should stop working after 30 days." What changes in the design?
Asked in

FAQ
Should URLShortener itself be a Singleton?
Only if the requirement genuinely needs exactly one shared instance app-wide (e.g. one in-memory counter must be globally consistent). In most real systems the counter and storage live in a shared database anyway, so `URLShortener` itself doesn't strictly need to be a Singleton — say this trade-off out loud rather than defaulting to Singleton automatically.
Why base-62 instead of base-64?
Base-64 includes `+` and `/`, which aren't safe in a URL path without extra encoding. Base-62 (letters + digits only) avoids that problem entirely, which is why it's the more common choice for this exact problem.
Is this the same design as the HLD version of this question?
No — an HLD version of this question would talk about load balancers, a distributed database, caching layers, and request throughput. This LLD version zooms into one part of that system — the classes inside the shortening service itself — exactly the distinction drawn in Lesson 1.
Next, the single most-asked LLD interview question — and the one where a "just add a class" plan falls apart fastest — Lesson 10: Design a Parking Lot →


