DataIntermediate

The URL shortener, built from its own brief

The system-design course designed it on paper. This is the same system as running code: a key generator that never collides, a redirect that is mostly a cache hit, and click counts that do not slow the redirect down.

The business problem

Take the url-shortener brief from the system-design course and build what it describes: create a short link, redirect it, count the clicks, expire what nobody uses. The brief's numbers are your load test's targets.

The bar is the redirect path. It must answer from a cache almost every time, never generate the same key twice under concurrent creation, and count a click without a synchronous write on the path the user is waiting on.

What you will have at the end

  • A key scheme you can defend: length, alphabet, collision handling, and what happens at a billion links
  • A redirect that is a cache hit with a measured hit rate, and a database read only on a miss
  • Click counting off the redirect path, with the number eventually right
  • Expiry and deletion that do not scan the table

Milestones

Each one ends in something you can observe. Without that a milestone is a heading, and you have no way to know you finished.

  1. Create, with a key that cannot collide

    Decide between a random key checked on insert (a unique index and a retry on conflict) and a counter encoded in base62. Write the decision down with the arithmetic from the brief.

    done whenA test creates 10,000 links from 50 threads and every key is unique; the database's unique constraint is what caught the collision, not application code.

  2. Redirect from the cache

    GET /{key} reads Redis first and the database on a miss, filling the cache with a TTL. Return 301 or 302 on purpose — the brief says which and why.

    done whenA load test at the brief's read rate shows a cache hit rate above 95% and p99 redirect latency under 10 ms, and you can say what the miss path costs.

  3. Count clicks off the path

    The redirect increments a Redis counter or publishes an event; a job folds counts into the database in batches. The redirect never waits for the database.

    done whenThe redirect's latency does not change when the click-fold job is stopped, and the counts catch up when it resumes.

  4. Expiry without a scan

    Links expire; a job deletes them. Index what the job queries on (expires_at) and page the deletes so no statement holds a lock across the table.

    done whenDeleting a million expired links runs in batches and the redirect's p99 is unchanged while it runs.

  5. The stampede and the missing key

    A famous link expires from the cache at peak; a key that does not exist is requested a million times. Handle both: single-flight rebuild for the first, a negative cache for the second.

    done whenA load test against a deleted key shows the database seeing a handful of reads, not one per request.

Data

One table: key (primary key, the short code), target URL, owner, created_at, expires_at, clicks. The interesting index is on expires_at for the deletion job; the primary key already serves the redirect.

Trade-offs you will have to defend

Random keys need a uniqueness check and retry; counter-based keys never collide and reveal how many links exist. Both are correct; the brief argues one.
301 is cached by browsers and cheap and makes click counts wrong; 302 is counted every time and costs a request. Pick by what the counts are for.
A Redis counter is fast and lost on a restart unless persisted; an event per click is durable and a queue to run. The tolerance for a lost click decides it.