Track 1 · Foundations
easyURL Shortener
Turn a long URL into a short code and redirect on lookup. The classic warm-up: tiny write path, enormous read path, and a key-generation problem hiding underneath.
Suggested architecture
Scenario
Two paths that have nothing in common. Reads outnumber writes 100:1, so the question is how much of the read path the cache absorbs — and what the store sees when it does not.
Links followed per second. This is the hot path; everything else is sized from it.
Links created per second. Small, but every one is a durable write.
Share of redirects the cache answers. Popularity is skewed, so a small cache gets a high number — until it restarts.
Stateless, so this is the knob you turn first.
Each partition of the key-value store handles a fixed rate before you shard again.
Click a component for its role, common technology choices and tradeoffs, and what it is carrying at this scale. Hover a connection to see what flows along it. Drag to rearrange — layout changes are local and reset on reload.
Every figure here is a rough estimate from simple capacity arithmetic, not a benchmark. Each part carries its own assumption about what one copy can do — real numbers depend on your hardware, payloads and access pattern. The point is which component moves first as you turn the dials, not the digits themselves.
In plain words
A URL shortener turns a long address into a short one and sends anyone who opens the short one to the long one. Making a link happens once; opening it happens millions of times. Nearly everything in this design comes from treating those two actions differently — and the lesson (find the busy path, then make it as small as possible) is one you will reuse in almost every system.
The shape of the problem
A URL shortener looks trivial and mostly is — but it is the cleanest example of a system whose two operations have nothing in common. Creating a link is a rare, write-once operation where nobody minds an extra half second. Following a link is constant, read-only, and every millisecond is felt. Almost every design decision here follows from refusing to treat those two paths the same way.
Assume the interview numbers: 100 M new links per day and a 100:1 read/write ratio.
- Writes: 100 M / 86 400 ≈ 1 200 writes/sec average, call it 3 500 at peak.
- Reads: ≈ 120 K reads/sec average, 350 K at peak.
- Storage: 100 M/day × ~500 bytes × 5 years ≈ 90 TB.
The write number is small enough that a single well-tuned database handles it. The read number is not, and that is where the design goes.
Generating the short code
Three approaches, and the choice matters more than it looks.
Hash the URL (MD5/SHA, take the first 7 chars). Simple and stateless, but collisions — two different URLs producing the same code — are certain at scale, so every write needs a read-before-write to check — which turns a cheap insert into a round trip, and the collision-resolution path is fiddly.
Auto-increment a counter, encode base62. Guaranteed unique, no collision check needed. But a single counter is a coordination point (one thing every server must talk to, which caps speed and fails alone), and sequential codes are guessable — anyone can walk your entire link database by counting up.
Lease ranges from a counter. Each service instance grabs a block (say
[1 000 000, 1 001 000)) from a coordination service — a small
consensus system — then hands out codes from
that block locally with no network calls. Uniqueness comes for free from the
disjoint ranges, throughput is unbounded, and codes are no longer densely
sequential. This is what the diagram shows.
Base62 (
a–z,A–Z,0–9) at 7 characters is 62⁷ ≈ 3.5 trillion codes. You can burn ranges carelessly on every crash and still not run out.
Why the read path is almost empty
The redirect service does one lookup and returns a 301. That is the entire hot path, and everything tempting to add — click analytics, spam checks, per-user counters — is deliberately kept off it. Analytics goes out as fire-and-forget (sent without waiting for a reply) or onto a queue; the user's redirect never waits for it.
Link popularity is very uneven: a small number of links take a large share of the traffic (a power law, if you want the name). That is exactly the shape a cache is good at. And because a short code's mapping is immutable once written, the usual hard part of caching — invalidation, knowing when a cached copy has gone stale — simply does not exist here.
301 vs 302
A permanent redirect (301) lets the browser cache the mapping, so repeat visits never reach you at all. That is a large, free traffic reduction. The cost is that you lose per-click analytics for cached visits, and you can never re-point or revoke that link for users who already have it. A temporary redirect (302) gives you both of those back and gives up the traffic reduction. Most commercial shorteners choose 302 for exactly this reason — analytics is the product.
Where this design breaks
- Cache cold start. Restart the cache fleet and 120 K reads/sec hit the store directly. Stagger restarts, or warm from a recent-keys list.
- A single link going viral. One key can exceed what one cache shard (one of the machines the cache is split across) can serve. The fix is replication of that key across shards, or an edge cache / CDN in front.
- Custom aliases. They break the collision-free property of range leasing — now you genuinely need a uniqueness check, on a separate path from generated codes.
- Deletes and expiry. Nothing in this design reclaims codes. If links expire, you need a TTL sweep and a decision about whether expired codes may be reissued (they should not be — old links in the wild would silently re-point).
Take this with you
- The one idea: separate the rare write path from the constant read path, and keep the read path down to a single lookup.
- In an interview, lead with the numbers (1 200 writes/s vs 120 K reads/s), pick range-leasing for codes, and be ready to defend 301 vs 302.
- At work, look for the same shape in your own system: a hot path with extras bolted on. Every one of them is latency you are paying on every request.