# System Design Drill Day 14: URL Shortener and Unique ID Generation

SMS is billed per 160-character segment, and a long tracking URL can push a single message into two. Over a whole marketing blast that adds up to real money, and that alone is reason enough for a marketplace to run its own short links.

The URL shortener is probably the best-known system design question, because it packs ID generation, read-heavy caching and async analytics into one small system. The one here is `shp.link`, the short-link service an online marketplace uses in its order SMS and marketing blasts.

## Core concepts

A shortener maps a short code to a long URL and redirects anyone who visits the code. The mapping itself is trivial. The design work is in generating the codes and handling the clicks.

Codes are usually base62 (a to z, A to Z, 0 to 9). Six characters give 62^6 ≈ 56.8 billion codes, and seven give 62^7 ≈ 3.5 trillion.

Every way of generating them has a catch. Encoding an auto-increment ID in base62 is simple, but the codes are sequential and guessable, and one database takes every write. Hashing the URL and cutting it to 7 characters means checking for collisions. A random code with a unique index, retried on a duplicate, is simple and hard to guess. Counter blocks, where each server reserves 1,000 IDs at a time with Redis `INCRBY`, are fast but still sequential underneath. Snowflake IDs (timestamp + machine ID + sequence) are roughly time-ordered, but clock skew between machines can bite.

Then the redirect status. A 301 is permanent and browsers cache it, so repeat clicks never reach you and you can't change or expire the target. A 302 is temporary, so every click passes through your server.

Traffic is read-heavy, with clicks usually outnumbering creates by about 100 to 1. Cache code→URL in Redis, and cache "not found" results briefly so scanners don't hammer the DB. Click analytics should never insert into the DB synchronously on the redirect path; push the click to a queue or bump a Redis counter, and write to the DB in batches.

> **Pause and think:** Why does a 301 quietly break click analytics?

**Answer:** Browsers cache a 301. The second time someone taps the link, the browser goes straight to the target without asking your server, so you never see that click. You also lose the ability to expire or retarget the link.

## The design problem

The marketplace wants short links in its order SMS and marketing blasts, sent through the Day 13 notification service.

Each marketing blast creates one link per recipient for click tracking, about 2M links a month, with a peak of 50k created in one minute. Clicks run to about 20M a month and peak around 2k/s right after a blast. Merchants need campaign click counts on a live dashboard. Order-tracking links must not be guessable, and links need to expire.

> **Try the problem before reading the solution.**

## Questions to think about

1. Why are sequential codes dangerous for order-tracking links?
2. 301 or 302 for `shp.link`?
3. How does MySQL survive the click spike right after a blast?

## Model solution

### Redirects

Serve code→URL from Redis, cache misses briefly, and rate-limit per IP (Day 7) to slow down anyone guessing codes. At 2k/s, Redis hits spread across 3 servers are easy to handle.

![Redirect path: a click on shp.link hits Laravel, which reads the code from Redis, falls back to MySQL on a miss, returns a 302, and pushes the click event to a Redis list for batching](https://cdn.hashnode.com/uploads/covers/5fdde6221485f22e24b34a2c/27c4d0a6-2c5a-4dae-97dc-d559b8fccecd.png align="center")

*Figure 1: The redirect path stays tiny: one Redis lookup, a 302, and a fire-and-forget click event.*

### 302, not 301

Use a 302 for every link. A cached 301 would cost you the repeat clicks in your analytics and the ability to expire or retarget a link.

### Click tracking

On each redirect, push the click to a Redis list, run `HINCRBY campaign:{id}:clicks`, and use `SET link:{code}:clicked 1 NX` to catch each recipient's first click for the unique-clicker count. A scheduled command drains the list every 30 to 60 seconds, bulk-inserts into `link_clicks` and upserts the totals. The live dashboard reads the Redis counters, and history comes from MySQL.

![Click analytics pipeline: redirects push to a Redis list and increment counters, a scheduled command drains the list every 30 to 60 seconds into MySQL, the live dashboard reads Redis and history reads MySQL](https://cdn.hashnode.com/uploads/covers/5fdde6221485f22e24b34a2c/7e9ab56f-79d3-401f-8b39-c4ed3c37f87f.png align="center")

*Figure 2: Live numbers from Redis, history from MySQL, and nothing slow on the redirect itself.*

### Expiry and safety

Check `expires_at` on every redirect, and have a nightly job delete or archive expired rows in chunks. Only the marketplace's backend can create links, and target domains are checked against an allowlist, so `shp.link` can't be abused as an open redirect.

### Capacity

2M links × about 500 bytes ≈ 1 GB a month, or about 3 GB across 90 days of retention, which is small. Redis carries the peak redirect load.

### Why it pays

`shp.link/aB3xK9pQ` is 17 characters against roughly 100 for a full tracking URL, which often keeps an SMS to one segment.

### Answers to the questions

1. Sequential codes let anyone add 1 and see someone else's order, and they reveal how many orders you take. Use random 8-character codes with a unique index.
2. 302, so every click is counted and links can expire or be retargeted.
3. Only cache misses touch MySQL, through one indexed lookup. Warming Redis when the blast's links are created (writing each code to Redis at creation time, before anyone can click), caching misses and writing clicks in batches keep MySQL nearly idle during the spike.

## Interview one-liner

> "State the read/write ratio and capacity numbers first. Then pick an ID strategy and name its trade-off, justify the 302, and keep analytics off the hot path."

## Conclusion

The redirect is one lookup, so most of the design effort goes into the codes and the clicks. I wouldn't use sequential codes on anything tied to an order, because they let anyone walk through other people's orders one number at a time.

## Try it yourself

Write a small base62 encoder in PHP and a `links` table with a unique `code` column. Generate random 8-character codes for 100,000 rows and count the collisions on insert. Expect zero: with 62^8 (about 2.2 × 10^14) possible codes, 100,000 random picks will almost never collide, so an empty count means the test worked. To actually see the unique index and the retry doing their job, repeat with 3-character codes.

Add a route that looks the code up in Redis first, falls back to MySQL, and returns a `302`. Hit it in a loop with `curl -I` and confirm the redirect every time, then switch to a `301`, open the link a few times in a real browser and check whether the repeat clicks still show up in the server logs.

---

This is part of my daily [System Design Drills](https://blog.anishansari.com.np/series/system-design-drills) series.
