The core design is done and the diagram looks right. Then the interviewer asks what happens when the cache restarts at peak, how a retried POST avoids making two links, and what the design costs per month. None of this changes the boxes. All of it decides whether the design survives a bad Tuesday.
This is the second half of Design a URL shortener, which builds the design itself. It holds the questions that fill the last fifteen minutes of the round.
- Design a URL shortener: number blocks, scrambled base62 codes, cache-aside reads and the 302 decision.
- Caching strategies and the CAP theorem help with the failure and consistency sections.
Why does the design return 302 instead of 301, and why is the number scrambled before it is encoded? A 302 keeps every click, expiry and takedown under the service’s control. The scramble stops anyone walking the codes in order.
- State assumptions before drawing. An unstated assumption looks like a missing requirement.
- Make creates safe to retry with an idempotency key, answer a taken alias with 409, and page through links with a cursor.
- For each part, say what breaks, by how much, and what saves you. Arithmetic beats adjectives.
- Redirects choose availability. Alias creation chooses consistency. Say both, and why.
- Alert on the error budget, not on single errors, and watch the cache hit rate because it is the database’s real load.
- Count clicks off the redirect path. Rotate the scramble key by versioning number ranges.
Extend a working URL shortener for production: ask the clarifying questions, design a retry-safe API and data model, tabulate failures with numbers, state the consistency stance, set alerts and a cost estimate, rotate the scramble key safely, and answer the usual deep dives.
Ask Before You Draw#
Spend the first two minutes on questions. Where the interviewer shrugs, state an assumption out loud and move on.
| Question | Assumption if no answer |
|---|---|
| Anonymous creates, or accounts? | Accounts, so quotas and listing work |
| Custom aliases? | Yes, with a minimum length |
| Default expiry? | None, but a user can set one |
| Can a link be edited? | No, create and disable only |
| Clicks counted? | Yes, daily totals, a few minutes late |
| How many regions? | One first, a second as a follow-up |
| Is abuse in scope? | Yes, at creation and by report |
These are judgement calls, not facts. The point is that each one changes a later decision: accounts enable rate limits per user, and “no editing” is what makes 302 caching safe.
API Detail That Interviewers Probe#
The core lesson had two endpoints. A production contract needs more.
POST /links is not idempotent by definition: RFC 9110 section 9.2.2 lists only safe methods, PUT and DELETE as idempotent. A request that times out and is sent again can create two links. Two fixes:
- Idempotency key. The client sends an
Idempotency-Keyheader, and the server stores the first response under(owner_id, key). Stripe may prune keys once they are 24 hours old and compares the parameters of a retry with the original. The header is still an Internet-Draft, not an RFC. - Deterministic alias. Derive the code from the owner and the normalised long URL, so the same request always lands on the same row.
| Case | Status | Note |
|---|---|---|
| Created | 201 | Body has code and short_url |
| Retry with same key | 201 or 200 | Stored first response |
| Alias taken | 409 | Conflict with current state |
| Malformed URL | 400 or 422 | Validate before storing |
| Not logged in | 401 | Blocked destination: 403 |
| Over quota | 429 | Add Retry-After (RFC 6585) |
For GET /links?limit=20&cursor=..., use a cursor over (owner_id, created_at, code), not an offset. Assume 20 links a page: page 500 makes an offset query skip 10,000 rows. A cursor seeks straight to the next row. Limit creates with a rate limiter.
Data Model and Indexes#
Assume a 500-byte row, as in the core lesson.
links(code PK, long_url, host,
owner_id, created_at,
expires_at, status)
idx_owner(owner_id,
created_at DESC, code)
idx_host(host) -- takedown
idx_expiry(expires_at)
idem(owner_id, key PK,
response, created_at)codeserves the redirect.idx_ownerserves the listing cursor.hostdisables every link to one bad domain in one statement.idx_expirylets cleanup skip a table scan.
Do not index long_url: nothing queries by it, and each index adds write cost.
What Breaks, By How Much#
Every number here is an assumption carried over from the core lesson: 120 writes a second and 12,000 reads a second at peak, a 90 percent cache hit rate, and replicas that serve about 2,000 reads a second each.
The cache cluster restarts at peak. How much load reaches the database, and how does it compare with three replicas? Answer, then read the first table row.
| Part | What happens | Arithmetic | What saves you |
|---|---|---|---|
| Cache down | Every read misses | 12,000 against 3 x 2,000 = 6,000 a second: twice capacity, against 1,200 a second normally | Coalescing, local cache, load shedding |
| Counter store down | No new blocks | Runway = block size / per-instance rate | Bigger blocks, alert on blocks left |
| Primary down | Writes fail until failover | Assume 30 s: 30 x 120 = 3,600 failed creates. Assume 1 s of lag: up to 120 links lost | Replica reads, retries, semi-sync |
| One replica down | Capacity falls | 2 x 2,000 = 4,000, still above the 1,200 normal | Spare replica |
Cache down is the stampede#
Assume one hot code gets 2,000 reads a second and a database fetch takes 5 ms. That puts 2,000 x 0.005 ≈ 10 fetches in flight for that one key. With request coalescing it is one. Also keep a short local cache in each server and cap concurrent database reads, so overload becomes a few slow answers instead of an outage.
Counter down is a sizing question#
Assume 4 instances share 120 writes a second, so 30 each.
- Assume a block of 1,000: it lasts ≈ 33 seconds at peak.
- Assume a block of 10,000: it lasts ≈ 333 seconds, or 5.5 minutes.
- Assume a crash wastes at most one block: 10,000 of 3.52 trillion codes.
Alert when an instance holds under half a block.
Primary down can lose links#
It loses data if replication is asynchronous. Sentinel’s documentation says acknowledged writes are not guaranteed to survive a failover. For links, that means a user holds a code that later returns 404. Because writes are only 120 a second, a semi-synchronous write path is affordable. State the RPO (about zero for links) and RTO (about 30 seconds, an assumption) you chose.
A failed-over counter can reissue numbers it already handed out. The UNIQUE constraint on code rejects the duplicate and the service takes a fresh number.
Consistency: Two Answers, Not One#
The honest stance is split by operation. In CAP terms:
| Operation | Choice | Why |
|---|---|---|
| Redirect read | Available, may be stale | A broken link breaks pages. Staleness is bounded by the cache TTL |
| Alias create | Consistent | A conditional insert on the primary. During a partition it fails instead of giving one alias to two users |
| Takedown | Fast, not instant | Stale for at most the shorter of TTL and edge max-age |
In PACELC terms, with no partition, reads trade consistency for latency (replicas and caches), while alias creation pays latency for consistency. This is design judgement, so say it as a choice you made, not a law.
Watch the Right Numbers#
Pick a few signals and give each a threshold.
- p99 redirect latency at the read service, against the 100 ms target assumed in the core lesson.
- Cache hit rate. The database load is
(1 - hit rate) x reads. Assume 90 percent: it is 1,200 a second. At 80 percent it doubles to ≈ 2,400. Alert at 85 percent (an assumption), where the load is 1,800, well inside the 6,000 limit. - Assumed redirect availability SLO of 99.9 percent. Assume 4,000 reads a second over 2.5 million seconds: ≈ 10 billion redirects a month, so the budget is 10 million failures, or 43.2 minutes of full outage in a 30-day month.
- Replication lag, blocks left per instance, abuse reports waiting.
Page on budget burn, not on single errors. The Google SRE Workbook recommends pairing a long and a short window: for a 99.9 percent SLO, a burn rate of 14.4 over one hour (confirmed over five minutes) pages, because it consumes 2 percent of the budget. Treat its numbers as a starting point to tune.
What It Costs#
Prices change, so these are dated and the rest are labelled assumptions. Assume the ≈ 10 billion redirects a month from above.
| Item | Arithmetic | Result |
|---|---|---|
| Edge requests | Cloudflare Workers paid plan, checked 2026-10-24: $5 a month, 10 million included, $0.30 per extra million. (10,000 - 10) x $0.30 + $5 | about $3,000 a month, request charge only |
| Egress | Assume 400 bytes per 302 and $0.09 per GB. 10 billion x 400 B = 4 TB | about $360 a month |
| Cache memory | Assume the hottest 1 percent of 6 billion links, 600 bytes each: 60 million x 600 B | about 36 GB |
The lesson is the shape, not the totals. Edge serving bills every redirect, so use it for the hot codes and let origin caches take the rest. Cloudflare’s page also bills CPU time separately, so read it before quoting a total.
Deep Dive: Counting Clicks at Scale#
Assume 4,000 clicks a second and 50 bytes per event: 200 KB a second, about 17 GB a day. Writing a database row per click would put a write on the hot path.
- Emit and move on. If the log is slow, drop the event. A redirect never waits for analytics.
- Aggregate before writing. Assume a link with 2,000 clicks a second: it becomes one write every 10 seconds per code.
- Expect duplicates. Logs deliver at least once, so dedupe on an event ID where counts feed billing. Charts can tolerate small error.
See event-driven architecture and Kafka vs RabbitMQ for the log choice.
Deep Dive: Abuse and Takedown at Scale#
At creation, check the destination against a reputation service such as Google Web Risk, which says its lists can miss risky sites and flag safe ones. So add layers:
- Rate limit and quota per user and per address.
- Re-scan popular links on a schedule, hottest first, because a page can turn bad later.
- Take down by host.
idx_hostdisables every link to one domain in one statement. - Purge every layer: mark the row disabled, delete the cache entry, purge the edge.
The worst-case exposure after a takedown is the cache TTL or the edge max-age, whichever is shorter. Assume 60 seconds for both and say so. A 301 would make this unbounded, which is why the core lesson chose 302.
Rotating the Scramble Key#
The key is a secret, so sooner or later someone asks how to change it. Existing links survive, because a redirect looks up the stored code and never reverses the scramble.
The one place that does reverse it is the cross-region trick from the core lesson, which decodes a code to a number to find its home region. That is what depends on the key.
Rotation protects codes made after the change. It cannot un-leak codes already issued under a leaked key. Moving from 6 to 7 characters works the same way: codes are strings, so old ones stay valid.
Practice: Size the Block#
Assume 4 instances, 120 writes a second at peak, and a counter store that can be down for 10 minutes. Work out:
- The block size each instance needs.
- The most codes wasted if all four crash.
- One alert that gives warning.
Hint
Per-instance rate is 120 / 4. Runway is block size divided by that rate.
One reasonable answer
rate = 120 / 4 = 30 per second
10 min = 600 s
block >= 30 x 600 = 18,000
choose = 20,000 (a round number)
wasted <= 4 x 20,000 = 80,000
share = 80,000 / 3.52e12 ~ 2e-8A block of 20,000 covers the outage with some margin, and a crash of every instance wastes about two billionths of the 7-character space. Alert when any instance holds under 10,000 numbers, which at 30 a second is about five minutes of warning. The trade-off is that bigger blocks waste more on a crash, which costs almost nothing here.
The 45-Minute Plan#
| Minutes | Do |
|---|---|
| 0 to 5 | Questions, assumptions, requirements, out of scope |
| 5 to 12 | Entities, API with idempotency and errors, estimate |
| 12 to 20 | Simple design, then the read path and cache |
| 20 to 32 | Deep dives: codes, redirect status, one failure table |
| 32 to 40 | Consistency stance, abuse and takedown, regions |
| 40 to 45 | Observability, cost, recap, follow-ups |
This is a suggestion. Cut cost before cutting the failure table.
What Each Level Should Show#
| Level | Expected in the follow-up round |
|---|---|
| Mid-level | Names the cache stampede and the 409 for a taken alias when prompted |
| Senior | Brings the failure table and idempotent creates unprompted, with numbers |
| Staff+ | Splits consistency by operation, rotates the key safely, sets SLO burn alerts, prices the edge |
Expect one-line follow-ups. Answer with a trade-off, not a tool name:
| They ask | Answer in one line |
|---|---|
| Client retries a create? | Idempotency key, stored response |
| Two users want one alias? | Conditional insert, loser gets 409 |
| Cache restarts? | Coalesce, local cache, cap database reads |
| Counter store dies? | Block size sets the runway |
| Primary dies? | Promote a replica, state RPO and RTO |
| A link goes viral? | Replicate the hot entry, edge absorbs it |
| Count clicks? | Log, aggregate, write per window |
| Phishing link reported? | Disable, purge every layer, ban the host |
| Leaked scramble key? | Rotate by range version, new codes only |
| How do you know it works? | Latency, hit rate, SLO burn |
Check Yourself#
1.Why is a database row per click a bad idea on the redirect path, and what replaces it?
It adds a write to the hottest path and a slow database can slow every redirect. Emit an event to a log, aggregate per code per window, and write the totals, so the redirect never waits.
2.The hit rate falls from 90 to 80 percent. What happens to database load?
It doubles from 1,200 to 2,400 reads a second at peak, because load is (1 - hit rate) times reads. That is why the hit rate gets an alert before the database does.
3.Why can the scramble key be rotated without breaking old links?
Redirects look up the stored code and never reverse the scramble. Only cross-region routing decodes a code, so it tries the new key, then the old, then asks other regions.
4.Why does alias creation choose consistency while redirects choose availability?
A stale redirect costs one slightly old answer, bounded by the TTL. Two users holding the same alias is a correctness bug, so the conditional insert fails during a partition rather than risk it.
Cover the table in the failure section and rebuild it from memory with the numbers. Do it again tomorrow. The arithmetic, not the part names, is what interviewers listen for.
In One Picture#
Ask first
accounts? aliases? regions?
API
idempotency key, 409, 429,
cursor paging
Failures
cache x2 over capacity;
block size = runway; RPO/RTO
Consistency
reads available,
alias consistent
Watch
p99, hit rate, SLO burn
Cost
edge bills every request
Rotate
version the number rangesConclusion#
The follow-up answer is everything around the read-path machine: a create that is safe to retry, a failure table with numbers, a split consistency stance, budget-based alerts, an honest cost line and a way to change the key. State the assumption each time and the answers stay short.
Which follow-up would catch you out? Tell us in the comments.
- Back to the core design: design a URL shortener for number blocks, caching and 302.
- The box guarding creates: design a rate limiter and rate limiting explained.
- Next problem: news feed design, another read-heavy system.

