Project 1 — URL shortener
coreintermediateBuild 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.
Think of it as
Two products sharing one database. A write product that is low volume, needs validation, rate limiting and abuse checks, and can afford to be slow; and a read product that is high volume, does one key lookup, and must be fast and boring. Designing them as one service is what makes people put the click counter in the redirect handler.
What we're doing: Get the redirect path right — the part that is one line of code and three decisions.
- 12
- A read-through cache changes the failure mode as well as the latency: Redis being down must degrade to slow, never to wrong. That only holds because Postgres remains the record.
- 22
- This is the general form of the rule, not a URL-shortener detail: a critical path may not synchronously depend on a system whose failure you would not page for.
- 31
- The 301 trade is the one people discover in production, months later, when the click graph flattens and nobody can explain why. It is a caching decision disguised as a status code.
Why this works: Nothing in the redirect path is difficult, which is exactly what makes it a good first project: every one of the three decisions has a defensible answer in both directions, and getting them wrong produces a system that works perfectly in testing and behaves strangely at scale.
Choosing the ID scheme by its length
Wrong
Better
What you see: Either an insert path that quietly retries more and more often as the table grows, or a competitor who can count your total links by shortening one URL a day.
Why: Code length is a consequence of the scheme, and the scheme is a consequence of one property — unguessable, shortest, or deduplicating. Picking the length first leaves the property unchosen, so it gets decided by whichever scheme happened to fit the length.
- 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
Three ID schemes, and the property each one buys
The two paths, sized separately
Remember: 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.
See also: url shortener end to end · id schemes and their tradeoffs · cache patterns · rate limit scope · distributed rate limiter

