InterviewKata
System design · Worked design D01 of 46 · Warm-up · Long form

Design a URL shortener

Turn https://example.com/a/very/long/path?with=parameters into sho.rt/x7Kp2Q, and send anyone who opens the short link to the long one, quickly, for years. It's the classic first design: small enough to finish, but with real depth in generating codes, serving a flood of reads, counting clicks without slowing anything down, and keeping printed links alive. It follows the interview framework phase by phase.

Modules this design uses03 Estimation · 07 API design · 09 Caching · 10 CDNs · 18 Queues · 20 IDs

1. The prompt

"Design a URL shortener like bit.ly or TinyURL. Users paste a long URL and get a short one; anyone opening the short URL is sent to the original. Link owners want to see how often their links are clicked."

The write side is tiny, and the read side is a flood that must never break, because short links end up printed on posters, packaging and business cards. Most of the interview is about the redirect path: how codes are made, how redirects stay fast and available, and how analytics stay out of the way.

2. Frame the problem

Ask these first. Each answer changes something specific; guess before you open them.

How many links are created, and how often are they opened?

About 100M new links a month, and roughly 100 redirects for every link created.

Writes are small (~40 a second); reads are 100× larger and bursty. The design is read-optimised.

Can users choose their own alias (sho.rt/spring-sale)?

Yes, optionally.

Aliases must be checked for uniqueness atomically, and must never clash with generated codes.

Do links expire?

Optionally, at a date the owner chooses. Most never expire.

Reads must check expiry, and something must clean up expired links without slowing anything.

Can the destination of a link be changed later?

Not in this version.

Immutable links can be cached forever, which makes the cache trivially correct.

What analytics do owners need?

Click counts per link, by day; country and referrer would be nice. A minute of delay is fine.

Analytics can be asynchronous, so counting stays out of the redirect path.

How short should codes be, and should they be guessable?

As short as practical, around 7 characters. Codes shouldn't be easy to walk through one by one.

This decides how codes are generated (section 9.1).

What about abuse?

Shorteners are used to hide phishing and malware. We must be able to block a link quickly.

We need a fast "disable" path through every cache layer, and checks when links are created.

3. What it must do

  1. A user can create a short link for a long URL, optionally with a custom alias and an expiry date.
  2. Anyone opening a short link is redirected to the long URL.
  3. The owner can see click counts for their links.

Not now:

4. How well it must do it

TargetWhy this number
Fast redirects: p99 under 50 ms at the server speedA redirect is pure overhead before the real page loads.
Redirects always work: 99.99% available; availability beats consistency (a new link taking a second to work everywhere is fine) consistency or availability · surviving failuresA broken short link breaks every page, email and poster it's printed on.
Never lost, never shared: a created link works for years; two links never get the same code data safetyLinks outlive campaigns; reusing a code would send people to the wrong place.
Not enumerable: codes can't be walked one by one to discover private links securityPeople shorten private documents and unlisted pages.
Read-heavy: ~100 redirects per created link, with viral spikes load shapeOne link shared by a celebrity can get thousands of clicks a second.

5. Numbers that change the design

QuantityWorking→ so we…
New links100M a month ÷ (2.5 × 10⁶ s)~40/s, ~120/s at peak → so one database handles writes easily
Redirects100 × writes~4,000/s, ~12,000/s at peak → so we cache hot links
Storage100M × 12 months × 5 years × ~500 bytes6 billion links ≈ 3 TB → so one large database or a key-value store; no sharding on day one
Code length62 characters (a–z, A–Z, 0–9); 62⁷ ≈ 3.5 × 10¹²→ so 7 characters lasts centuries at this rate (check with the calculator in section 13)
Cache~350M redirects a day, mostly for recently shared links: say 50M hot links × 500 bytes~25 GB → so one Redis cluster holds the hot set

6. The nouns

links        code (primary key), long_url, owner_id, created_at, expires_at, status (active | disabled)
click events code, time, referrer, country             (a stream, not a table updated per click)
click_counts code, day, clicks                          (written by the aggregation job)

7. The contract

REST. The owner comes from the auth token. The redirect is a plain HTTP GET on the short domain, because that's what browsers do.

POST /v1/links                 Idempotency-Key: …     {long_url, custom_alias?, expires_at?}
  → 201 {code: "x7Kp2Q", short_url: "https://sho.rt/x7Kp2Q"}     (409 if the alias is taken)
GET  /{code}                   → 302 Found, Location: https://example.com/a/very/long/path…
                                 (404 if unknown, 410 Gone if expired or disabled)
GET  /v1/links/{code}/stats    → {clicks_total, clicks_by_day: [...]}          (owner only)

8. First working version

Stateless app servers behind a load balancer and one database. Build each request, then note what will break.

8.1 Creating a link

For the first version, generate 7 random base62 characters and insert with the code as the primary key; if the insert fails because the code exists, generate another. A custom alias uses the same insert, and the primary key rejects one that's taken.

8.2 Redirecting

Look the code up by primary key, check status and expiry, and answer 302 with the long URL in Location.

8.3 Counting clicks (version one)

To start, each redirect also runs UPDATE click_counts SET clicks = clicks + 1 WHERE code = ? AND day = ?, and the stats page reads those rows.

Parked for later (section 9 fixes each)

  1. Random codes collide more often as the table fills.
  2. Every redirect reads the database, so a database blip breaks every link.
  3. A write on every redirect puts row contention on viral links into the fastest path.
  4. Should the redirect be 301 or 302?
  5. Aliases, expiry and abuse need rules.
  6. One region: a regional outage breaks every printed link.

9. Make it hold up

9.1 Generating unique short codes never shared · not enumerable

ApproachWhy
✗ BadHash the long URL (MD5) and take the first 7 charactersDifferent URLs can collide, and the fix (check, then rehash) is messy; the same URL from two users gets the same code, leaking who else shortened it and breaking per-owner stats.
~ Good7 random base62 characters, insert with a unique constraint, retry on collisionSimple and unguessable. Collisions are rare while the table is small but grow as it fills (the birthday problem), so later inserts may need retries.
✓ GreatA counter, encoded in base62. Each write server leases a block of 1,000 IDs at a time from a central counter (a database sequence or Redis INCR), so the counter isn't touched per request. Scramble the number with a reversible permutation before encoding so codes don't look sequential.Guaranteed unique, no collision checks, short codes, and the counter is hit only once per 1,000 links. A server that crashes wastes the rest of its block, which is fine.

Try both calculators in section 13: how long codes last, and what a scrambled counter looks like.

9.2 Redirects in under 50 ms, at 12,000 a second fast · always work

ApproachWhy
✗ BadEvery redirect reads the databaseWorks at first, then the database becomes the bottleneck at peaks, and a database blip breaks every link.
~ GoodCache-aside in Redis: code → long_url, LRU eviction, entries never stale because links don't changeMost redirects are for a small set of popular links (a Zipf distribution), so a modest cache serves the large majority in under a millisecond.
✓ GreatCache, plus redirects answered at the CDN edge for popular links (short cache lifetimes), an in-process cache for the very hottest codes, and read replicas or a replicated key-value store behind itA viral link is served from edges near users; the origin sees only misses. Redirects keep working even if the primary database is down.

Keep the create path and the redirect path as separate services. They have different traffic, different scaling and different failure tolerance, and the redirect service should have no dependency it doesn't absolutely need.

9.3 301 or 302? fast · analytics

A 301 Moved Permanently lets browsers and proxies cache the redirect, so repeat visits never reach you: less load, but you can't count those clicks or block the link later for people who already have it cached. A 302 Found (or 307) is checked each time: you see every click, at the cost of more traffic. Shorteners that sell analytics use 302, with a short Cache-Control lifetime so CDNs can still absorb spikes. Say which you chose and why.

9.4 Counting clicks without slowing redirects fast · analytics

ApproachWhy
✗ BadUPDATE … clicks = clicks + 1 on every redirectPuts a write (and row contention on viral links) in the hottest read path.
~ GoodIncrement a counter in Redis; flush to the database every minuteFast, but a cache failure loses counts, and it only gives totals.
✓ GreatThe redirect service emits a click event to Kafka after responding (fire-and-forget, with a local buffer); a stream job aggregates counts per link per minute into an analytics storeRedirects stay fast; detailed stats (by day, country, referrer) come for free; the raw log can be reprocessed. Counts are seconds behind, which the requirements allow. Design D17 goes deeper.

Clicks answered at the CDN edge never reach the origin; count them from CDN logs, merged into the same pipeline.

9.5 Aliases, expiry and abuse never shared · security

9.6 Keeping printed links alive across regions always work · never lost

9.7 The whole design after the deep dives

Both paths through the final design: creating a link with a leased counter block, and redirecting through the edge, the cache and the database, with the click going to analytics.

10. Interviewer follow-ups

Traffic grows 10×: 1 billion links a month.

Writes reach ~400 a second, still modest. The links table grows to ~30 TB over five years, so partition it by hashing the code (or use a key-value store that does it for you). Counter blocks per region or per server keep generation local. Push more redirects to the edge; the origin load grows far less than 10× because popular links are served from caches.

One link goes viral: 1M clicks a minute.

That's one key. The CDN serves it from every edge with a short cache lifetime, and the redirect service keeps the hottest codes in process memory, so neither Redis nor the database notices. Analytics events arrive in a burst; Kafka absorbs it.

Owners now want to change a link's destination.

Add a version to the link. Edits bump it, and the redirect service caches by code with a short lifetime (or listens for invalidation events). CDN entries are purged on edit. It costs more cache misses, which is why it was "not now".

Should the same long URL always get the same code?

Not across users: it would merge their stats and reveal what others shortened. Per user, you can dedupe with an index on (owner_id, hash(long_url)) if the product wants it.

How would you count unique visitors?

Exact uniques need storing every visitor per link. A HyperLogLog per link per day estimates them with ~1% error in about 12 KB each, and the sketches can be merged across days (Design D20).

A link is reported as phishing. How fast can you stop it?

Mark it disabled, delete it from Redis, purge it from the CDN, and broadcast an invalidation to the redirect servers' in-process caches. All of this takes under a minute. That's why we chose 302: a 301 would stay cached in browsers.

Support custom domains (go.acme.com/offer).

Make the key (domain, code) instead of just the code. Map each customer domain to the service with DNS and issue TLS certificates automatically. The redirect path looks the domain up first. Alias uniqueness becomes per domain.

Move from PostgreSQL to a key-value store without downtime.

Dual-write new links to both, backfill old links in batches, compare reads in the background (shadow reads), then switch reads over, and finally stop the old writes. Redirects never stop, because the cache sits in front of both.

Why not generate codes from a timestamp?

Timestamps collide when two servers create links in the same millisecond, and they reveal when a link was made. A Snowflake-style id (time + machine + sequence) avoids collisions but is longer than 7 characters. Leased counter blocks are shorter.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Links database primary downCan't create links; redirects keep workingWrite errors; failover eventRedirects served by caches and replicas; a replica is promoted
Redis cluster lostSlightly slower redirectsCache-miss rate jumpsDatabase and replicas absorb misses; the cache refills by itself
Counter service unavailableCreation slows after blocks run outBlock lease errorsServers hold leased blocks; fall back to random codes with retry
Kafka slow or downStats a few minutes behindConsumer lagRedirect servers buffer events locally; nothing on the redirect path waits
A region goes downNothing, for redirectsRegional health checksDNS or anycast moves traffic to other regions
Phishing campaign using our linksBlocked links show a warning pageReports, threat-feed matchesDisable everywhere within a minute; creation rate limits

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch:

At 10×:

A 45-minute interview

0–4Frame the problem (scale, aliases, expiry, analytics)
4–9Must do, how well, numbers
9–14Nouns and contract
14–25First working version 8.1–8.3, parked list
25–40Deep dives: codes (9.1), fast redirects (9.2), 301/302 and analytics (9.3–9.4)
40–45Follow-ups and wrap-up

A 60-minute interview

0–5Frame the problem
5–12Must do, how well, numbers
12–17Nouns and contract
17–28First working version
28–50Deep dives 9.1–9.6 and the final picture
50–60Follow-ups: viral link, custom domains, migration; wrap-up
LevelWhat a strong answer shows
Mid-levelClear framing, the two main APIs, a working design with a database and a cache, and a sensible code-generation scheme with collision handling.
SeniorSeparate read and write paths; counter blocks or random codes defended with numbers; 301 vs 302; analytics off the hot path; hot links at the edge; a fast disable path.
Staff+Drives the trade-offs: storage choice and migration, multi-region redirects, abuse handling as a first-class requirement, operational concerns (cache warm-up, the domain itself), and what changes at 10×.

Common mistakes:

13. Try it, check yourself, read more

How long do codes last?

7
100M

Encode a counter value

The product sells click analytics. Which redirect status?

301s are cached by browsers, so repeat clicks never reach you.

How do you keep a central counter from becoming a bottleneck?

Each server takes 1,000 IDs at a time and hands them out locally.

Best way to count clicks?

Keeps writes out of the latency-critical redirect path and gives detailed stats.

The links database primary fails. What happens to redirects?

The redirect path only reads, so caches and replicas serve it while creation waits for failover.
ResourceWhy
Design a URL shortener like Bit.ly (Hello Interview)Another walkthrough with common follow-ups.
Design Pastebin and bit.ly (System Design Primer)A worked solution with estimates and code generation.
Redirections in HTTP (MDN)301, 302, 307 and 308, and how browsers cache them.