03. URL Shortener — Interview Q&A¶
~15 min read · Part 4 of 4 (Overview → HLD → LLD → Q&A)
These are the questions an interviewer actually asks once the diagram is on the board. Each answer is the strong version, followed by the wrong answer that quietly sinks candidates.
Q1. How do you generate short codes without collisions? Allocate integers from a monotonic counter and base62-encode them, but vend the counter in blocks: a central allocator hands each app server a range of a few thousand IDs, and servers mint codes locally from their block. Disjoint ranges make collisions impossible by construction, and the allocator is touched once per block rather than once per write, so it never becomes a bottleneck. If codes must not leak growth rate, permute the integer with a reversible transform before encoding. Common wrong answer to avoid: "Hash the URL and take the first 7 characters." Hashes collide, so you inherit a check-and-retry loop on every write that degrades exactly under load.
Q2. Why put a cache in front if the database can already read fast? Because the load is extremely read-heavy (≈100:1 reads to writes) and highly repetitive — the same popular codes are requested over and over. A cache turns the common redirect into a single in-memory lookup and shields the store from viral spikes. The database is sized for durability and cold misses, not for absorbing 40k reads/second of hot traffic. Common wrong answer to avoid: "Modern databases are fast enough, so a cache is optional." It works until one link goes viral and a single hot key saturates the store.
Q3. 301 or 302 for the redirect?
It is a genuine tradeoff. 301 (permanent) lets browsers and CDNs cache the redirect aggressively, which maximizes performance and offloads origin — but you stop seeing those clicks, so analytics undercount and deletes don't propagate to clients that cached the response. 302 (temporary) sends every click back to you, keeping analytics accurate and links revocable, at the cost of more origin traffic. Default to 302 so revocation and analytics work, and offer 301 as an opt-in for links the owner declares permanent.
Common wrong answer to avoid: "Always 301 because it's faster." Faster, yes — but it breaks click analytics and makes deletion unreliable.
Q4. A Black Friday campaign link is doing 30k redirects/second — how does the system survive it? Recognize first that this is read amplification, not database load: one code requested 30,000 times a second. Three layers stack. The CDN caches the redirect at the edge, so the origin sees maybe 1,000/s of the 30,000; Redis serves that remainder from memory as a hot key; and the one dangerous moment — when the entry expires mid-sale and 30,000 requests arrive cold in the same second — is contained by single-flight coalescing, so exactly one request reads the store and the other ~29,999 wait a few milliseconds for the fill. The result is that the hottest link is the cheapest to serve, because it never leaves edge and memory. Common wrong answer to avoid: "Just scale up the database" or "shard the store." Vertical scaling buys a little headroom and sharding spreads distinct keys — neither helps one hot key, which only caching and coalescing address.
Q5. How do you enforce uniqueness for custom aliases under concurrent requests?
Let the store enforce it atomically with a conditional insert (INSERT … ON CONFLICT DO NOTHING, or a put-if-absent in a KV store). The primary-key constraint is the single arbiter of "taken." A read-then-write check in application code has a race window where two requests both read "free" and both write.
Common wrong answer to avoid: "Check if it exists, then insert." That check-then-act is a classic race; two concurrent requests can both pass the check.
Q6. Would you store analytics in the same table as the mapping? No. Redirect lookups need low-latency point reads by key; analytics is append-heavy and queried by aggregation over time and dimensions. Mixing them lets analytics write load and heavy aggregate queries degrade redirect latency. Emit click events to an event bus and land them in a separate append-optimized (columnar/time-series) store. Common wrong answer to avoid: "One table with a click_count column, incremented per redirect." A synchronous counter update on every redirect adds a write to the hot read path and creates contention on popular rows.
Q7. When do you shard the mapping store, and on what key? Shard when storage, throughput, or blast-radius on one node grows uncomfortable — not on day one. The short code is the natural partition key: base62 codes distribute uniformly, so shards fill and are read evenly, and every hot query is already a lookup by that key. Traffic skew (hot links) is handled by the cache/CDN, not by the partition scheme. Common wrong answer to avoid: "Shard by owner_id" or "shard immediately." Owner-based sharding skews with power users and doesn't match the read pattern; premature sharding adds operational cost before it's needed.
Q8. How much storage and what keyspace do you need? At 100M new links/month and ~500 bytes/row, that's ~50 GB/month, ~600 GB/year — comfortably a few TB over the service's life, well within a sharded store. For codes, 7 base62 characters give 62⁷ ≈ 3.5 trillion combinations, enough for centuries at that rate, so 7 characters is a safe fixed length. Common wrong answer to avoid: Hand-waving "we'll need a huge distributed database." The data volume is modest; the interesting scaling is on the read path, not storage.
Q9. What happens when Redis goes down? Reads fall through to the store, so redirects keep working with higher latency and higher store load. Mitigations: cluster Redis with replicas so a single node loss is survivable; lean on the CDN to keep absorbing the hottest links; and rely on single-flight coalescing so the cold-cache window doesn't turn into a store-crushing stampede. Common wrong answer to avoid: "Redirects go down." They shouldn't — the cache is an accelerator, and the durable store plus CDN keep the read path alive when it fails.
Q10. How do you delete or take down a malicious link quickly?
Set the row's status to disabled (soft delete) so reads return 404, then actively invalidate the Redis entry and purge the CDN path rather than waiting for TTL. Keep an admin kill-switch that does exactly this in one action. Because 302 links re-ask the origin on every click, they stop resolving immediately; 301 links may linger in client caches, which is one more reason to default to 302.
Common wrong answer to avoid: "Just delete the database row." That leaves the link resolving from cache and CDN until entries expire, which is unacceptable for abuse response.
Q11. How do you keep a nonexistent code from hammering the store? Negative-cache 404s with a short TTL. A flood of requests for a bogus (or guessed) code then short-circuits in Redis instead of repeatedly missing through to the store. The TTL is kept short so a code that gets created shortly after becomes resolvable quickly. Common wrong answer to avoid: "Only cache successful lookups." Then every request for a bad code is a guaranteed cache miss and a store read — a cheap denial-of-service vector.
Deeper follow-ups¶
- How would you make link creation idempotent so client retries don't mint duplicate codes?
- How would you support geo-aware redirects (send EU users to a different destination) without hurting cache hit rate?
- How would you detect and rate-limit an account mass-creating links for spam?
- If you needed globally low-latency redirects, how would you replicate the mapping store across regions, and what consistency would you accept?
- How would you migrate the code length from 7 to 8 characters without breaking existing links?
- What changes if the same long URL must map to one shared code (deduplication) instead of per-user codes?
How this round is scored¶
Interviewers use the URL shortener to see whether you let the traffic shape drive the design. The strong signal is recognizing early that this is a read-heavy, skew-heavy problem and building the caching/CDN story around it, rather than over-engineering the write path or reaching for a "big data" store the volume doesn't justify. Seniority shows up in the tradeoff discussions — 301 vs 302, hash vs counter, cache TTL vs deletion — where you name both sides and pick with a reason. The failure-mode section (hot keys, Redis loss, abuse takedown) separates candidates who have run systems from those who have only drawn them. Doing the back-of-envelope math out loud, and using it to justify choices rather than as decoration, is what pushes an answer from "correct" to "senior."