Ein durchgearbeitetes Design
Alles zusammen an einem Problem - der Entwurf eines skalierbaren URL-Shorteners - von Anforderungen und Abschätzung über das Datenmodell, Caching, Sharding bis zu den Abwägungen, die du verteidigen würdest.
Deutsche Übersetzung in Arbeit
Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.
Auf dieser Seite
Time to assemble everything. We'll design a URL shortener (think TinyURL/bit.ly) end to end, using the exact approach from this module: requirements, estimation, high-level design, deep dives, and trade-offs. It's the canonical system-design problem because it's small enough to finish yet touches every tool - estimation, caching, sharding, the read-heavy pattern, and real trade-offs to defend.
Step 1: requirements
Functional:
- Given a long URL, produce a short one (e.g.
short.ly/aB3xK9). - Given a short URL, redirect to the original.
- Optional: custom aliases, expiry, click analytics. (We'll note but not deep-dive these.)
Non-functional:
- Massively read-heavy - people click short links far more than they create them (classic ~100:1).
- Low-latency redirects - a redirect must be fast (it's on the user's critical path).
- High availability - a dead link shortener is useless; a slightly stale redirect is harmless (leans AP).
Step 2: estimation
Assume 100M new URLs/day, 100:1 read:write (from the capacity lesson):
Writes: 100M/day ÷ ~100,000 s ≈ ~1,000 writes/sec
Reads: ~100,000 reads/sec ← very read-heavy → CACHE + replicas
Storage: ~500 bytes/URL × 100M/day × 365 × 5 yr ≈ ~90 TB ← too big for one node → SHARDThe estimate already dictates the architecture: heavy caching, read replicas, and sharded storage. We didn't guess
- the numbers told us.
Step 3: the core deep-dive - generating the short key
The interesting problem is the short code. A 7-character code over base62 (a-z, A-Z, 0-9) gives 62^7 ≈ 3.5 trillion combinations - plenty. Two main approaches:
- Hash the URL (e.g. take the first chars of a hash) - but hashes collide, so you must detect and resolve collisions, adding read-before-write cost.
- A unique counter / ID, base62-encoded - a global sequence produces a unique number per URL; encode it to base62 for the short code. No collisions by construction. At scale, a single counter is a bottleneck, so use a distributed ID generator (e.g. a range-allocator handing each server a block of ids, or a Snowflake-style id).
The counter approach is cleaner (no collision handling), so we'll use a distributed ID generator → base62 encode. This is the deep-dive the problem actually turns on - not the load balancer.
Step 4: high-level design and the data path
┌─────────── Cache (Redis) ────────────┐ ← absorbs the 100K reads/sec
Client ─► Load ─► App Servers ─► (cache miss) ─► Database (sharded by short code)
Balancer │ ▲
└── write path: ID gen → base62 → store (shortCode → longUrl)Write (create): app server gets a unique id from the ID generator, base62-encodes it to the short code,
stores shortCode → longUrl in the database, returns the short URL. ~1,000/sec - easily handled.
Read (redirect): the hot path, ~100,000/sec. App server looks up the short code in the cache first; on a hit (the overwhelming majority - popular links dominate, 80/20), return the long URL and issue an HTTP redirect. On a miss, read the database, populate the cache (cache-aside), and redirect. Because short-code → long-URL mappings are immutable, caching is trivially safe - no invalidation problem (the hard part of caching simply doesn't arise here). This is why the design is fast: the read-heavy load is served almost entirely from memory.
Step 5: scaling and trade-offs
Now tie in the tools and defend the choices:
- Database: ~90 TB and 100K reads/sec - shard by short code (the natural lookup key), so each redirect hits
exactly one shard (no cross-shard queries on the hot path - a deliberately shard-friendly key). Add read
replicas per shard for the read volume the cache misses. A key-value store fits (simple
shortCode → longUrllookups, easy horizontal scaling) - a reasonable NoSQL choice. - Caching: the linchpin. Cache-aside in Redis; immutable mappings mean long TTLs and no invalidation headache. This is what makes 100K reads/sec affordable.
- CAP trade-off: choose AP - during a partition, keep serving redirects even if a just-created link hasn't propagated everywhere yet. Justification: availability of redirects matters far more than a few seconds' consistency on a brand-new link. Defensible because a stale/missing brand-new link is a minor, self-correcting inconvenience, while downtime breaks every existing link.
- Analytics (the async part): counting clicks synchronously on the hot redirect path would slow it and couple it to the analytics store. Instead, fire a message to a queue on each redirect and process click counts asynchronously (load-leveling + decoupling) - the redirect stays fast, analytics catches up.
Notice how every module showed up: estimation sized it, caching carried the reads, sharding held the data, CAP framed the consistency call, and a queue absorbed the analytics. And every choice came with a why - which is the entire skill.
In a design discussion, narrate the trade-offs - that's the signal
Getting to a working design is table stakes; what distinguishes a senior engineer is saying WHY at each fork: 'I'm choosing a counter over hashing to avoid collision handling,' 'redirects are read-heavy and immutable so caching is the win and invalidation isn't a problem,' 'I'll take AP here because a stale new link is harmless but downtime isn't.' There's no single correct design - there's a design whose trade-offs you can defend against the stated requirements. Practice narrating the reasoning, not just drawing the boxes.
The finished design is like a superbly-run cloakroom at a huge venue. Coats come in at a trickle (writes) and each gets a unique tag number (the base62 id - no two alike, by construction, so no mix-ups to resolve). Retrieval is the rush (reads): most people reclaim a handful of popular coats near the front, kept on a fast rack by the counter (the cache), so the attendant rarely walks to the deep storage racks (the database). The racks are organized by tag number across several rooms (sharding by short code) so any tag points straight to one room - no hunting across all of them. And rather than pausing to log each retrieval, the attendant drops the used tag in a bin to be tallied later (the analytics queue), keeping the line moving. Every earlier technique becomes one part of a cloakroom that stays fast under a crowd - and you can explain why each part is there.
Your URL shortener works. Now a new requirement lands: links can now expire (a user sets a TTL, after which the short link must stop redirecting and return 'expired'), and premium users can edit a short link to point to a new destination. How do these two changes affect the design - specifically the caching that was previously so clean? What would you adjust?
In the URL shortener design, why is caching so effective and simple for the redirect path?
Key takeaways
- A URL shortener ties the whole module together: requirements → estimation → high-level design → deep-dive the key generation → scaling trade-offs.
- The estimate (100:1 reads, ~90 TB) dictates the architecture - heavy caching, read replicas, and sharding - rather than being guessed.
- The real deep-dive is short-code generation: a distributed ID generator + base62 encoding avoids the collision handling that hashing requires.
- The design is fast because redirects are read-heavy AND the mapping is immutable, so cache-aside serves nearly all reads with no invalidation problem; shard by short code so each redirect hits one shard.
- Seniority is narrating the trade-offs (counter vs hash, AP vs CP, async analytics via a queue) - there's no single correct design, only one whose trade-offs you can defend.