Back to path
BeginnerLinkly · Project 3 of 12 ~3.5h· 5 milestones

Scale its reads before they melt the database

Continues from the last build: Rung 2 left you with a correct API, data model, and short-code generator, but every single redirect still hits Postgres directly.

Rung 2 shipped a correct design: a clean API, a solid data model, and a short-code generator that does not collide.

cache-aside patternTTL and eviction policy designcache hit ratio capacity maththundering herd / stampede mitigationstaleness and consistency tradeoffsRedis sizing and key designgraceful degradation on dependency failurewriting an ADR

What you'll build

A defended cache-aside design: an ADR naming the decision and its risks, a capacity sheet with real hit-ratio math (not an assumed 100 percent), a TTL and eviction policy with a stated staleness contract, and an explicit mitigation for cache miss stampedes and Redis outages.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

adr/003-cache-aside-redis.mdmarkdown
# ADR 003: Add a Cache-Aside Layer for Redirect Reads

## Status
Proposed

## Context
Linkly's redirect path (GET /r/:code) now runs at roughly 100:1 reads to writes.
Every single redirect still calls Postgres directly to resolve short_code to
long_url. That was fine at rung 2's traffic, but reads have no ceiling and
writes do, because they share the same primary. The database is becoming a
single point of contention before it becomes a single point of failure.

## Options considered
1. Do nothing, scale Postgres vertically (bigger box, buys time not architecture)
2. Add Postgres read replicas now (this is rung 5's move, too early, doesn't fix hot-key skew)
3. Add a cache-aside layer (Redis) in front of the DB for hot redirect lookups

## Decision
<TODO: confirm option 3 and cite the hit-ratio math from the capacity sheet>
We cache short_code to long_url pairs in Redis, read-through on miss, with the
app owning both the read and the write path (cache-aside, not read-through or
write-through at the datastore layer).

## Consequences
- Reads that hit the cache never touch Postgres, so DB load drops by the hit ratio
- Freshness is bounded by TTL, not instant. Edits to a short code must actively invalidate the key, not just wait out the TTL
- A cold cache or a mass expiry creates a stampede risk that needs its own mitigation (see milestone 5)
- Redis becomes a new dependency. When it is down, reads must degrade to hitting Postgres directly, not fail outright

Reading this file

  • roughly 100:1 reads to writesThis ratio is why the read path, not the write path, is today's bottleneck.
  • 2. Add Postgres read replicas now (this is rung 5's move, too early, doesn't fix hot-key skew)Naming the option you're rejecting, and why, is what makes an ADR defensible later.
  • <TODO: confirm option 3 and cite the hit-ratio math from the capacity sheet>Never state a decision without the number that justifies it.
  • A cold cache or a mass expiry creates a stampede risk that needs its own mitigation (see milestone 5)Consequences must name the new failure mode you're introducing, not just the win.

Fill in the TODOs once you've done the math in the capacity sheet.

That's 1 of 6 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Build the capacity case for a cache

    4 guided steps

    Adding a cache because 'caches make things faster' is a guess. Adding a cache because peak read QPS is 2.5x over the database's measured ceiling is a decision you can defend in an interview or a design review.

  2. 2

    Decide the cache-aside pattern and write the ADR

    4 guided steps

    There are several caching patterns and picking one without saying why is the number one way a design review stalls a discussion. Cache-aside is the right default here because the app already owns both the read and write path.

  3. 3

    Pick the eviction policy, TTL, and staleness contract

    4 guided steps

    A cache with no eviction plan either falls over (default noeviction rejects writes once full) or grows unbounded. A cache with no TTL plan either never refreshes (stale forever) or thrashes (refreshes so often it barely helps). Both are decisions, not defaults.

  4. 4

    Redo the capacity math with a real hit ratio and update the diagram

    4 guided steps

    The single most common mistake on this rung is math that quietly assumes a perfect cache. A hit ratio is a variable that depends on TTL and traffic skew, and the design has to survive the case where it is lower than hoped.

  5. 5

    Design for a cache miss stampede and a Redis outage

    4 guided steps

    A cache with no stampede story turns its own expiry into a self-inflicted DDoS on the database. A cache with no failure story turns Redis into a new single point of failure for the whole read path, exactly what the cache was supposed to prevent.

What's inside when you start

2 starter files, ready to clone
5 guided milestones
4 full reference solutions
6 code blocks explained line-by-line
5 "is it working?" checks
4 interview questions it prepares you for

You'll walk away with

An ADR (adr/003-cache-aside-redis.md) with the Decision and Consequences sections filled in
A capacity sheet (capacity/cache-math.md) showing DB load before the cache, and after, at multiple realistic hit ratios with a stated crossover point
An updated architecture diagram including the cache tier, the cache-aside request flow, and a hit-ratio metric
A written stampede mitigation (request coalescing) and Redis-outage fail-open behavior

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building