Jump to a problem, path or page
Jump to a problem, path or page
01 · The reference design · HLD
Use a stateless API, durable link mappings, cache-aside redirects, and asynchronous click processing. The diagram shows logical dependencies; production services may have multiple instances behind a load balancer.
6 components · 5 connections. One valid design among several; compare the reasoning, not just the shape.
02 · The reasoning
03 · The details
POST /links { destination, expiresAt? } → 201 { code, shortUrl, expiresAt }
GET /:code → 302 Location: destination | 404 unknown | 410 expired
Link { code PRIMARY KEY, destination, createdAt, expiresAt nullable }
ClickEvent { eventId, code, occurredAt }Core flow · illustrative pseudocode, not an executable implementation.
create(destination, expiresAt):
validateHttpUrl(destination); validateExpiry(expiresAt)
repeat up to 5 times:
code = secureRandomBase62(8)
if database.insertUnique(code, destination, expiresAt): return code
throw RetryableCodeAllocationError
redirect(code, now):
link = cache.get(code) or database.find(code)
if link is absent: return 404
if link.expiresAt != null and now >= link.expiresAt: return 410
cacheBestEffort(link, ttlBoundedByExpiry(link, now))
enqueueClickBestEffort(code, timeout=short, queue=bounded)
return 302 with Location = link.destination04 · From model to implementation
Why the implementation works
The unique code index is the final allocation authority: checking availability before insert still races. Cache entries are derived copies, so both cached and database records must pass the same expiry check. Keep analytics admission bounded because a queue outage must not consume all redirect workers.
Follow a concrete scenario
Suppose code Ab12 expires at 12:00. A request at 11:59:59 can redirect, but a cached copy at 12:00 returns 410. If analytics is unavailable, the redirect still succeeds and a dropped-event counter increases. Retrying a colliding creation allocates a different code without overwriting the first destination.
05 · Why these choices
How do you allocate short codes?
Random Base62 with a unique constraint and collision retry
Random codes need a database uniqueness guarantee and bounded retries. Time alone and URL truncation can collide.
How should the cache behave?
Cache-aside with TTL bounded by link expiry
A hot mapping avoids database reads, but its cache TTL must not outlive the link. Enforce expiry on redirect too.
What happens if analytics is unavailable?
Redirect and use a bounded event queue with a documented loss policy
Keep analytics off the redirect critical path. Bound retries and queue growth, and state when events may be dropped.
06 · Size the workload
Use the exercise workload as a planning envelope. The daily volume assumes the stated write rate is sustained for 86,400 seconds and every write adds one record of the stated size; updates, deletions and sampling change retained growth. These are arithmetic estimates, not measured service capacity.
100 writes/s × 86,400 s/day8,640,000 writes/day
100 writes/s × 500 B/write × 86,400 s/day ÷ 1,000,000,0004.32 GB/day before indexes, replicas, compression and retention
10,000 reads/s with a target read p99 of 100 msMeasure end-to-end tail latency, then allocate deadline budgets across the actual critical path. A p99 target is not a mean service time and cannot size concurrency by itself.
07 · Pressure-test the design