Learn / System design
The two warm-up designs
Lesson 10 of 37 · 11 min read ·
Why these two
Both are small enough to design in fifteen minutes and deep enough to expose everything: id generation, storage choice, caching, hot keys, and what happens across multiple servers. Do them on paper before you read anyone's model answer — reading a solution feels like learning and is not.
Design 1: a URL shortener
Requirements. Shorten a long URL to a short one; redirect on visit; optionally track clicks. Assume 100M URLs stored and a 100:1 read:write ratio. Reads must be fast; writes need not be.
The short code. Three options, and the trade is the whole exercise:
- Hash the URL (MD5, take 7 chars). Deterministic, so the same URL yields the same code — but collisions exist and must be handled.
- Random 7 chars from a 62-character alphabet. 62⁷ ≈ 3.5 trillion, so collisions are rare but possible; you need a uniqueness check on insert.
- Auto-increment id, base62-encoded. No collisions ever, shortest possible codes — but sequential, so codes are guessable and enumerable, and a single counter is a scaling bottleneck. Fix with per-server id ranges or Snowflake-style ids.
Say which you picked and why. There is no correct answer; there is only an unjustified one.
Storage. A key-value lookup by short code, and nothing else. Any store works; the access pattern is trivially cacheable. code → (long_url, created_at, user_id, expires_at) with code as the primary key.
Read path. Cache the mapping in Redis. Mappings are immutable, so invalidation is free — the hardest cache problem simply does not exist here. Expect a hit rate above 95% because link popularity is extremely skewed.
Redirect. 301 is cached by browsers forever, which makes redirects free but kills your click tracking. 302 means every click reaches you. Pick based on whether analytics matter.
The follow-ups you will be asked. Custom aliases (uniqueness check, reserved words). Expiry (TTL plus a cleanup job). Analytics (do not write synchronously — push to a queue). Abuse (rate limit creation, scan against a malware list).
Design 2: a rate limiter
Requirements. Allow N requests per user per window, reject the rest with 429, work correctly across many application servers.
Fixed window. A counter per (user, minute). One INCR in Redis with a TTL. Dead simple, and wrong at the boundary: a user can send N at 11:59:59 and N at 12:00:00 — 2N in one second.
Sliding window log. Store a timestamp per request in a sorted set, drop entries older than the window, count what remains. Exactly correct. Memory grows with request volume, which is the cost.
Sliding window counter. Weight the previous window's count by how far into the current one you are. Approximate, cheap, and what most production limiters actually use.
Token bucket. A bucket of N tokens refilling at a fixed rate; each request takes one. Allows bursts up to bucket size while bounding the sustained rate. Usually the best fit for an API, because real clients are bursty.
The distributed part — this is the actual question. Per-server counters do not work: five servers each allowing 100/min means 500/min. You need shared state (Redis), and the check-then-increment must be atomic, or you have the race condition from the concurrency lesson. Use INCR (atomic by itself) or a small Lua script for multi-step logic.
Failure mode. Redis is down — do you fail open (allow everything, risking overload) or fail closed (reject everything, causing an outage)? Answer deliberately. Most APIs fail open with a local fallback limit.
Response. Return 429 with Retry-After, plus RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset so well-behaved clients can back off instead of hammering you. More in rate limiting and idempotency.
Prove you know it
Do both on a single sheet of paper in 15 minutes each: boxes, arrows, the data model, and one sentence per decision explaining the trade. Then read the system-design-primer versions and list only the things you missed. That list is your actual study plan.