1.Project 1 — URL shortener
Build a service that turns a long URL into a short code and redirects that code back to the original. It looks small, and that is why it is the first project: the whole thing fits in a weekend, but it forces four decisions that recur everywhere. The first is the read/write asymmetry — redirects outnumber creations by a factor of hundreds or thousands, so the create path and the redirect path get designed separately rather than as two endpoints on the same service. The second is ID generation, which has no obviously correct answer until you name the property you want: a random code is unguessable but needs a collision check, a counter in base62 is short and dense but leaks how many links exist and needs coordination between instances, and a hash of the URL deduplicates identical links but makes two users share one code and therefore one set of analytics. The third is caching, because the redirect path is a pure key lookup and belongs in Redis — with the database still the system of record, so a cache flush costs latency rather than data. The fourth is that analytics must not be on the redirect path: a click writes an event to a queue and the redirect returns, because a redirect that waits on an analytics write has coupled its availability to a system nobody would page for.
Key features
- Reads dominate by orders of magnitude — design the redirect path and the create path as separate concerns
- ID scheme is a property choice: random (unguessable, needs collision retry) · counter in base62 (short, leaks volume, needs coordination) · hash (dedupes, but shares analytics between users)
- Redis holds code → URL as a read-through cache; PostgreSQL stays the system of record so a flush costs latency, not data
- Click analytics goes on a queue — a redirect that waits on an analytics write inherits its availability
- Rate limit creation by API key or IP; the abuse case is bulk-generating links for phishing, not heavy redirect traffic
- Expiry and deletion need a decision up front: a deleted code returns 410, and reusing it later would resurrect old links
Three ID schemes, and the property each one buys
The two paths, sized separately
- Client
- leads to Create API (POST /links)
- leads to Redirect service (GET /{code})
- Create API — POST /links · rate limited
- leads to ID generation (next code)
- leads to PostgreSQL (insert)
- ID generation — base62 · scheme is a choice
- PostgreSQL — system of record
- Redirect service — GET /{code} → 302
- leads to Redis (lookup)
- leads to Click events (click event)
- Redis — code → URL, read-through
- leads to PostgreSQL (on miss)
- Click events — fire and forget
- leads to Analytics store (aggregate)
- Analytics store
Command
Build order — each step works before the next
1 POST /links + GET /{code}, Postgres only
2 ID generation: pick a scheme, write down
which property you chose it for
3 Redis read-through on the redirect path
4 Click events -> queue -> analytics store
5 Rate limit on POST /links (per API key)
6 Expiry: TTL column + a sweep, 410 on
an expired code
Definition of done: you can state your p99
redirect latency, what happens when Redis is
down, and why you chose 301 or 302.A URL shortener is two products sharing a database: a low-volume create path that needs validation and rate limiting, and a very high-volume redirect path that is one cached key lookup. Choose the ID scheme by the property you want (unguessable / shortest / deduplicating), keep Postgres as the record with Redis read-through so a flush costs latency and not data, push click events to a queue so redirect availability never depends on analytics, and choose 301 versus 302 deliberately — 301 is cached forever and silently ends your click counting.