Jour 42 Day 42 · mardi 6 octobre 2026 Tuesday 6 October 2026 Architecture Intermédiaire
System design junior : l'URL shortener Junior system design: the URL shortener
L'exercice de system design le plus donné aux juniors : dérouler la méthode (besoins, ordres de grandeur, schéma, itération) sur le cas bit.ly — et comprendre ce que l'interviewer évalue vraiment. The system design exercise most often given to juniors: apply the method (requirements, orders of magnitude, diagram, iteration) to the bit.ly case — and understand what the interviewer really evaluates.
L’essentiel
Un entretien de system design junior n’évalue pas votre connaissance d’architectures exotiques : il évalue votre façon de raisonner. L’interviewer veut vous voir clarifier un problème flou, poser des chiffres, proposer quelque chose de simple qui marche, puis l’améliorer là où ça coince. Un candidat qui dessine Kafka et douze microservices en trente secondes échoue ; un candidat qui commence par « combien d’URLs par jour ? » marque des points avant d’avoir dessiné quoi que ce soit.
🎤 En entretien — la méthode en 4 étapes, à dérouler à voix haute : 1. Clarifier les besoins (fonctionnels et non fonctionnels : volumétrie, latence, disponibilité). 2. Estimer les ordres de grandeur (requêtes/s, stockage — un calcul de coin de table suffit). 3. Dessiner le schéma simple qui répond au besoin. 4. Itérer sur les goulots d’étranglement, dans l’ordre où ils apparaîtraient. Annoncer le plan dès le début : l’interviewer voit que vous avez une démarche, pas des réflexes.
L’URL shortener (bit.ly, tinyurl) est le cas d’école : périmètre compréhensible en une phrase, mais assez riche pour toucher API, génération d’identifiants, stockage, cache, redirections HTTP et montée en charge.
Étape 1 — les besoins. Fonctionnels : créer un lien court depuis une URL longue ; rediriger le lien court vers l’original ; (bonus) compter les clics. Non fonctionnels : la lecture domine massivement l’écriture (ratio ~100:1), la redirection doit être rapide (< 100 ms), le service doit être disponible — un lien mort est un lien inutile.
Étape 2 — les ordres de grandeur. Hypothèse : 100 M de nouvelles URLs par an ≈ 3 écritures/s, donc ~300 lectures/s avec le ratio 100:1. Stockage : 100 M × ~500 octets ≈ 50 Go par an. Conclusion à énoncer tout haut : ça tient sur une seule base Postgres bien indexée — le « scaling » sera du confort de lecture, pas une question de survie.
Comment ça marche
L’API — deux endpoints suffisent :
POST /shortenavec{ "url": "https://…" }→201et{ "code": "aZ3k9x1" }(valider l’URL, refuser les schémas dangereux).GET /:code→301ou302vers l’URL longue,404si le code n’existe pas.
La génération du code — le cœur de l’exercice. Deux approches à comparer :
- Compteur + base62 : un id auto-incrémenté, encodé sur l’alphabet
[0-9a-zA-Z]. Simple, aucune collision possible, codes courts. Défaut : les codes sont prévisibles (on peut énumérer les URLs des autres) — se corrige en mélangeant l’id avec une permutation ou un offset secret. - Hash de l’URL (MD5/SHA tronqué à 7 caractères) : pas de compteur central, la même URL redonne le même code. Défaut : la troncature crée des collisions (paradoxe des anniversaires) — il faut vérifier en base et ré-essayer avec un salt.
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode_base62(n: int) -> str:
"""Encode un id auto-incrémenté en code court."""
if n == 0:
return ALPHABET[0]
out = []
while n:
n, r = divmod(n, 62) # reste = index dans l'alphabet
out.append(ALPHABET[r])
return "".join(reversed(out)) # 125 → "21", 10**9 → "15ftgG"
# 62^7 ≈ 3,5 × 10^12 codes sur 7 caractères :
# à 100 M/an, l'espace dure ~35 000 ans. Largement.
Le stockage — le modèle est une table clé-valeur : code (PK) → url, created_at, user_id?. Aucune jointure, aucune transaction complexe : n’importe quel store convient. Postgres suffit très largement à cette échelle ; un store clé-valeur (DynamoDB) ne devient pertinent qu’à l’échelle « milliards ». Le dire ainsi montre que vous dimensionnez au besoin, pas au CV.
L’architecture — version simple, puis itérée :
┌───────────────┐
client ──▶│ load balancer │
└───────┬───────┘
▼
┌──────────────────────┐
│ app servers │
│ (stateless, scale │
│ horizontal) │
└────┬────────────┬────┘
1. hit? │ │ 2. miss
▼ ▼
┌────────────┐ ┌───────────────┐
│ Redis │◀─│ DB code → URL │
│ (hot URLs) │ │ + réplicas │
└────────────┘ └───────────────┘
Les serveurs applicatifs sont stateless : tout l’état vit en base et en cache, on peut en ajouter derrière le load balancer sans rien coordonner. Pour la lecture : des réplicas de la base et le cache absorbent les 300 req/s sans effort.
Concepts clés à maîtriser
- 301 vs 302 — le vrai piège de l’exercice :
| 301 Moved Permanently | 302 Found (temporaire) | |
|---|---|---|
| Cache navigateur | Agressif, souvent définitif | Pas de cache par défaut |
| Clics suivants | Vont direct à la cible | Repassent par le service |
| Analytics | Perdues après le 1er clic | Comptées à chaque clic |
| Charge serveur | Minimale | Chaque clic touche le service |
| À choisir si | Aucun besoin de stats | Le tracking est un besoin (cas réel de bit.ly) |
- Cache des hot URLs — la popularité des liens suit une loi de Zipf : une petite fraction des codes concentre l’essentiel du trafic. Un Redis en cache-aside (on lit le cache, sur miss on lit la base et on remplit, avec un TTL) absorbe la majorité des lectures. Les URLs étant immuables, l’invalidation — le problème dur du caching — disparaît presque.
- Rate limiting — indispensable sur
POST /shorten: sans lui, un spammeur génère des millions de liens (phishing, pollution de l’espace de codes). Un token bucket par IP ou par clé API, et un429 Too Many Requests. - 404 et validation — un code inconnu renvoie 404 ; une URL d’entrée se valide (schéma http/https uniquement — sinon vous venez de créer un open redirect vers
javascript:). - Ce que l’interviewer évalue vraiment — dans l’ordre : vous clarifiez avant de dessiner ; vous posez des chiffres ; chaque brique du schéma a une justification (« un cache parce que la lecture domine ») ; vous connaissez les limites de votre design. Le raisonnement bat les buzzwords à tous les coups.
💡 Commencer simple est une compétence — « une base Postgres et deux serveurs suffisent à cette échelle » est une meilleure réponse d’entretien que n’importe quelle architecture distribuée non justifiée. Vous montrez que vous savez quand la complexité devient nécessaire — c’est exactement ce qui distingue un futur bon ingénieur.
En entretien
« Conçois-moi un raccourcisseur d’URL. » — Dérouler la méthode : besoins (2 endpoints, lecture >> écriture), chiffres (3 écritures/s, 300 lectures/s, 50 Go/an), schéma simple (LB → app stateless → Postgres + Redis), itérations (cache, réplicas, rate limiting). Annoncer le plan avant de commencer.
« 301 ou 302 pour la redirection ? » — 301 est « correct » sémantiquement et économise du trafic, mais le navigateur le met en cache : tous les clics suivants échappent au service, donc plus d’analytics. Si le tracking compte — c’est le business model de bit.ly — on choisit 302 (ou 301 assumé si on ne veut aucune stat). Montrer le trade-off vaut plus que la « bonne » réponse.
« Comment tu génères le code court ? » — Compteur + base62 : simple et sans collision, mais prévisible (corrigible par permutation secrète). Hash tronqué : pas de compteur central mais collisions à gérer (vérifier + retry). À cette échelle, compteur + base62 gagne ; 7 caractères = 62⁷ ≈ 3,5 × 10¹² codes.
« Que se passe-t-il si ta base tombe ? » — Les lectures survivent partiellement grâce au cache (les hot URLs répondent encore) ; les écritures échouent — acceptable brièvement. Ensuite : réplica promu en primaire, et le dire simplement suffit à un niveau junior.
« Comment tu empêches les abus ? » — Rate limiting sur la création (token bucket par IP/clé API), validation stricte des URLs, éventuellement une liste noire de domaines de phishing et un délai d’expiration des liens gratuits.
Pièges & idées reçues
⚠️ Le 301 qui tue les analytics — c’est LE piège tendu de l’exercice. Répondre « 301 parce que la redirection est permanente » sans mentionner le cache navigateur, c’est rater le point : après le premier clic, le navigateur ne repassera plus jamais par votre service. Si on vous demande ensuite « et comment tu comptes les clics ? », il est trop tard.
- La soupe de buzzwords — Kafka, microservices, sharding et CQRS pour 3 écritures/s : l’interviewer y voit du plaquage de mots-clés, pas de l’ingénierie. Chaque brique doit répondre à un chiffre.
- « Un hash est unique » — tronqué à 7 caractères, non : le paradoxe des anniversaires rend les collisions probables bien avant d’épuiser l’espace. Toujours prévoir la détection et le retry.
- Optimiser l’écriture d’un système de lecture — le ratio 100:1 dicte tout le design (cache, réplicas). Sharder les écritures ici, c’est résoudre un problème qui n’existe pas.
- Oublier la sécurité du produit — accepter n’importe quelle URL fait de vous un relais de phishing avec une belle réputation de domaine. Validation, rate limiting, expiration.
- Dessiner avant de questionner — se jeter sur le tableau blanc sans demander la volumétrie est l’erreur numéro un. Les deux premières minutes de questions sont celles qui rapportent le plus de points.
Pour aller plus loin
- System Design Primer — Design Pastebin/Bit.ly : la solution détaillée du repo de référence
- ByteByteGo — System Design Interview : la newsletter et les schémas d’Alex Xu, dont le chapitre URL shortener de son livre
- MDN — Redirections HTTP : 301, 302, 307, 308 et leurs sémantiques exactes
- S’entraîner : refaire l’exercice sur un cas voisin (pastebin, système de likes) en chronométrant les 4 étapes — 35 minutes, conditions réelles
The essentials
A junior system design interview doesn’t evaluate your knowledge of exotic architectures: it evaluates your way of reasoning. The interviewer wants to watch you clarify a fuzzy problem, put numbers on it, propose something simple that works, then improve it where it strains. A candidate who draws Kafka and twelve microservices in thirty seconds fails; a candidate who starts with “how many URLs per day?” scores points before drawing anything at all.
🎤 In an interview — the 4-step method, to run out loud: 1. Clarify the requirements (functional and non-functional: volume, latency, availability). 2. Estimate orders of magnitude (requests/s, storage — a back-of-the-envelope calculation is enough). 3. Draw the simple diagram that meets the need. 4. Iterate on the bottlenecks, in the order they would appear. Announce the plan up front: the interviewer sees you have a process, not just reflexes.
The URL shortener (bit.ly, tinyurl) is the textbook case: a scope you can grasp in one sentence, yet rich enough to touch API design, ID generation, storage, caching, HTTP redirections and scaling.
Step 1 — requirements. Functional: create a short link from a long URL; redirect the short link to the original; (bonus) count clicks. Non-functional: reads massively dominate writes (~100:1 ratio), the redirect must be fast (< 100 ms), the service must be available — a dead link is a useless link.
Step 2 — orders of magnitude. Assumption: 100M new URLs per year ≈ 3 writes/s, hence ~300 reads/s with the 100:1 ratio. Storage: 100M × ~500 bytes ≈ 50 GB per year. Conclusion to state out loud: this fits on a single well-indexed Postgres — “scaling” will be read-side comfort, not a matter of survival.
How it works
The API — two endpoints are enough:
POST /shortenwith{ "url": "https://…" }→201and{ "code": "aZ3k9x1" }(validate the URL, reject dangerous schemes).GET /:code→301or302to the long URL,404if the code doesn’t exist.
Code generation — the heart of the exercise. Two approaches to compare:
- Counter + base62: an auto-incremented id, encoded over the
[0-9a-zA-Z]alphabet. Simple, no collision possible, short codes. Drawback: codes are predictable (you can enumerate other people’s URLs) — fixed by scrambling the id with a permutation or a secret offset. - Hashing the URL (MD5/SHA truncated to 7 characters): no central counter, the same URL yields the same code. Drawback: truncation creates collisions (birthday paradox) — you must check the database and retry with a salt.
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode_base62(n: int) -> str:
"""Encode an auto-incremented id as a short code."""
if n == 0:
return ALPHABET[0]
out = []
while n:
n, r = divmod(n, 62) # remainder = alphabet index
out.append(ALPHABET[r])
return "".join(reversed(out)) # 125 → "21", 10**9 → "15ftgG"
# 62^7 ≈ 3.5 × 10^12 codes with 7 characters:
# at 100M/year, the space lasts ~35,000 years. Plenty.
Storage — the model is a key-value table: code (PK) → url, created_at, user_id?. No joins, no complex transactions: any store fits. Postgres is more than enough at this scale; a key-value store (DynamoDB) only becomes relevant at “billions” scale. Saying it that way shows you size for the need, not for the résumé.
The architecture — simple version first, then iterated:
┌───────────────┐
client ──▶│ load balancer │
└───────┬───────┘
▼
┌──────────────────────┐
│ app servers │
│ (stateless, scale │
│ horizontally) │
└────┬────────────┬────┘
1. hit? │ │ 2. miss
▼ ▼
┌────────────┐ ┌───────────────┐
│ Redis │◀─│ DB code → URL │
│ (hot URLs) │ │ + replicas │
└────────────┘ └───────────────┘
The app servers are stateless: all state lives in the database and the cache, so you can add servers behind the load balancer without coordinating anything. For reads: database replicas and the cache absorb the 300 req/s effortlessly.
Key concepts to master
- 301 vs 302 — the real trap of the exercise:
| 301 Moved Permanently | 302 Found (temporary) | |
|---|---|---|
| Browser cache | Aggressive, often permanent | Not cached by default |
| Subsequent clicks | Go straight to the target | Come back through the service |
| Analytics | Lost after the 1st click | Counted on every click |
| Server load | Minimal | Every click hits the service |
| Pick it if | Zero need for stats | Tracking is a requirement (bit.ly’s real case) |
- Caching hot URLs — link popularity follows a Zipf distribution: a small fraction of codes concentrates most of the traffic. A Redis in cache-aside mode (read the cache, on miss read the DB and fill it, with a TTL) absorbs the majority of reads. Since URLs are immutable, invalidation — caching’s hard problem — nearly disappears.
- Rate limiting — essential on
POST /shorten: without it, a spammer generates millions of links (phishing, code-space pollution). A token bucket per IP or API key, and a429 Too Many Requests. - 404 and validation — an unknown code returns 404; an input URL gets validated (http/https schemes only — otherwise you just created an open redirect to
javascript:). - What the interviewer really evaluates — in order: you clarify before drawing; you put numbers down; every box in the diagram has a justification (“a cache because reads dominate”); you know your design’s limits. Reasoning beats buzzwords every single time.
💡 Starting simple is a skill — “one Postgres and two servers are enough at this scale” is a better interview answer than any unjustified distributed architecture. You’re showing you know when complexity becomes necessary — exactly what separates a future good engineer.
In an interview
“Design a URL shortener for me.” — Run the method: requirements (2 endpoints, reads >> writes), numbers (3 writes/s, 300 reads/s, 50 GB/year), simple diagram (LB → stateless app → Postgres + Redis), iterations (cache, replicas, rate limiting). Announce the plan before starting.
“301 or 302 for the redirect?” — 301 is semantically “correct” and saves traffic, but the browser caches it: all subsequent clicks bypass your service, so no more analytics. If tracking matters — it’s bit.ly’s business model — pick 302 (or a deliberate 301 if you want no stats at all). Showing the trade-off is worth more than the “right” answer.
“How do you generate the short code?” — Counter + base62: simple and collision-free, but predictable (fixable with a secret permutation). Truncated hash: no central counter but collisions to handle (check + retry). At this scale, counter + base62 wins; 7 characters = 62⁷ ≈ 3.5 × 10¹² codes.
“What happens if your database goes down?” — Reads partially survive thanks to the cache (hot URLs still answer); writes fail — acceptable briefly. Then: a replica is promoted to primary, and saying it that simply is enough at junior level.
“How do you prevent abuse?” — Rate limiting on creation (token bucket per IP/API key), strict URL validation, possibly a blocklist of phishing domains and an expiry policy for free links.
Pitfalls & misconceptions
⚠️ The 301 that kills analytics — this is THE planted trap of the exercise. Answering “301 because the redirect is permanent” without mentioning the browser cache misses the point: after the first click, the browser will never come back through your service. When the follow-up “and how do you count clicks?” arrives, it’s too late.
- Buzzword soup — Kafka, microservices, sharding and CQRS for 3 writes/s: the interviewer reads it as keyword-dropping, not engineering. Every box must answer to a number.
- “A hash is unique” — truncated to 7 characters, no: the birthday paradox makes collisions likely long before the space runs out. Always plan detection and retry.
- Optimizing writes in a read-heavy system — the 100:1 ratio dictates the entire design (cache, replicas). Sharding writes here solves a problem that doesn’t exist.
- Forgetting product security — accepting any URL turns you into a phishing relay with a nicely reputed domain. Validation, rate limiting, expiry.
- Drawing before questioning — jumping to the whiteboard without asking about volume is mistake number one. The first two minutes of questions are the ones that earn the most points.
Going further
- System Design Primer — Design Pastebin/Bit.ly: the detailed solution from the reference repo
- ByteByteGo — System Design Interview: Alex Xu’s newsletter and diagrams, including the URL shortener chapter of his book
- MDN — HTTP redirections: 301, 302, 307, 308 and their exact semantics
- Practice: redo the exercise on a neighboring case (pastebin, a like system) while timing the 4 steps — 35 minutes, real conditions