cswork / libraryOpen practice workspace

Articles

URL Shortener System Design Interview: What Interviewers Actually Probe

The URL shortener system design interview, from 9 real asks: base62 vs hash collision math, 301 vs 302 redirects, hot keys, and ID block allocation.

"Design a URL shortener" looks like the friendliest system design prompt you can draw, which is exactly why it is dangerous. The interviewer has run it dozens of times and has a script of escalations ready: what happens when your MD5 codes collide, why you picked a 301 over a 302, what a single viral link does to your cache.

Key Takeaways

  • A counter encoded in base62 beats hash truncation for short-code generation. Seven characters give you 3.5 trillion codes and zero collisions by construction; with truncated MD5 you should expect your first collision around the two-million-link mark.
  • Default to 302 and be ready to defend it. A 301 lets browsers cache the redirect indefinitely, which erases your click analytics and makes a malicious link impossible to kill for anyone who already clicked it.
  • The system is read-heavy, often 100:1. Interviewers grade the redirect path: cache strategy, hot-key handling, and what happens on a cache miss at p99.
  • Scale your answer to the round. The same title covers whiteboard-scope design prompts from Amazon and Coinbase, a design-then-build onsite at xAI, and a local in-memory exercise at Shopify. Asking "how many links, what read volume" in the first two minutes is how you calibrate.
  • Gaps in your ID space are free. Block-allocating counter ranges to write nodes and burning unused IDs on crash costs you nothing against a 3.5-trillion-code namespace, and it removes the single point of contention.

Know which of the three formats you are walking into

Asked at xAI — Design and Implement a URL Shortening Service After several HR screens, the candidate got a 30-minute technical screen that was pure system design on a URL shortener, with no coding at all. The onsite kept the same focus: 30 minutes of whiteboard design with AI tools banned, then about 90 minutes implementing the design with AI allowed, then a half-hour wrap-up and demo. The new-grad pipeline had no algorithm rounds anywhere.

Asked at Shopify — Implement URL Shortening Codec A pair-programming phone screen for an ML engineer role: implement the encode/decode pair for shortening URLs, similar to the classic LeetCode codec problem. AI assistance in the IDE was explicitly allowed, so the bar shifted from raw coding speed to communication: understanding what the interviewer wanted built before building it, and handling the follow-ups they layered on.

The same six words on your interview schedule can mean three different rounds, and preparing for the wrong one is the most common way strong candidates fail this question.

Format one: the classic whiteboard. Boxes, arrows, and capacity math. The Amazon, Microsoft, Remitly, and Coinbase prompts in our bank all read as this scope, and the J.P. Morgan interview report pins its version to a 45-minute slot inside a longer loop. Depth wins: collision math, cache policy, failure modes.

Format two: design then build. xAI's version above is the clearest example: you commit to a design under whiteboard conditions, then live with your choices for ninety minutes of implementation. A design you cannot build in the time box is a wrong answer here even if it would score well in format one. If you have an xAI loop coming, our xAI interview guide covers how their rounds fit together.

Format three: the codec coding question. Shopify's phone screen is a coding problem wearing a system design costume. The rubric is clean code, an honest uniqueness guarantee, and how you respond to follow-ups — not sharding.

What transfers across all three is the core mapping: a generation scheme that produces short unique codes, and a store that resolves them fast. Get those two right and you can stretch or shrink everything else to fit the round.

Do the capacity math, then actually use it

Asked at Coinbase — Design a scalable URL shortener with expiration and security Design for hundreds of millions of users with a hard floor of 100,000 requests per second. Every link carries its own expiration, and an expired link must never resolve. The prompt asks specifically for the data model, storage schema, and indexing choices, plus how you keep redirects low-latency and safe under high concurrency.

Coinbase hands you the 100,000 RPS number to see whether you notice what kind of load it is. Run the arithmetic: even a large shortener creating 100 million links a month averages under 40 writes per second — call it 400 at peak. If the system is seeing 100k requests per second, essentially all of it is reads. The ratio is somewhere near 100:1 or steeper, and that single observation should shape every choice you make afterward.

Storage is similarly unscary. A row is a 7-character code, a long URL averaging maybe 100 bytes, two timestamps, and an owner ID: under 200 bytes raw, budget 500 with index overhead. Five years at 100M links a month is 6 billion rows, roughly 3 TB. That fits on one well-provisioned machine. You partition and replicate for read throughput and availability, not because the data will not fit.

The failure mode interviewers see constantly: a candidate spends four minutes producing these numbers, writes them in the corner, and never references them again. The math exists to justify decisions. "Reads dominate 100:1, so I am going to spend most of my time on the redirect path and put a cache in front of the store" is the sentence that converts arithmetic into signal.

Short-code generation is a counter problem, not a hashing problem

Asked at Bytedance — Design a TinyURL-like short link service After a half-hour resume deep-dive, the interviewer opened a blank editor and gave 25 minutes to generate TinyURLs in working code. The candidate reached for MD5, got working output that was far too long, and then hit the follow-up: what do you do about hash collisions? Believing MD5 "would not collide," they froze and pivoted to talking about TTLs instead.

That Bytedance follow-up is the single most reliable escalation in this interview, and the MD5 trap is worth understanding precisely.

A full MD5 is 128 bits: 22 characters even in base64, uselessly long for a short link. So candidates truncate to 6 or 7 characters. Seven base62 characters is about 2^41.7 of space, and the birthday bound says you should expect your first collision after roughly the square root of that: around two million links. Not a tail risk. A certainty, early, at any real scale. Truncated hashing forces you to bolt on a check-and-retry loop with a salt: a synchronous uniqueness check against the store on every create, a retry path when it hits, and the burden of making both race-safe under concurrent writes.

The cleaner answer is a monotonically increasing integer ID encoded in base62. It is a bijection (every ID maps to exactly one code), so collisions are impossible by construction, and the code stays as short as the ID allows: 62^6 is about 56.8 billion codes, 62^7 about 3.5 trillion.

ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def encode(n: int) -> str:
    if n == 0:
        return ALPHABET[0]
    out = []
    while n:
        n, r = divmod(n, 62)
        out.append(ALPHABET[r])
    return "".join(reversed(out))  # O(log n) time and space

Trace it on 11157: divmod gives remainders 59, 55, 2 from the bottom up, which index to X, T, 2 — so encode(11157) returns "2TX", and 2·3844 + 55·62 + 59 checks back out to 11157.

The counter's one genuine weakness is predictability: sequential codes let anyone enumerate your links by incrementing, which matters if short links carry private documents. Say this unprompted — it is the strongest signal in the section. The fix is a bijective scramble: multiply the ID, modulo 62^7, by a large constant that is coprime to 62 (odd and not a multiple of 31, since 62 = 2·31). Coprimality is what makes the multiplication invertible, so the map stays collision-free; an odd multiple of 31 would silently produce collisions. Alternatively, hand out counter blocks and randomize order within each block.

StrategyCollisionsLength at 1B linksEnumerable?Needs
Counter + base62None by construction6 charsYes — add a bijective scrambleKey allocator
Truncated hash (MD5/SHA to 7 chars)Expected by ~2M links7 charsNoUniqueness check + salted retry
Random code + checkSame birthday exposure7 charsNoUniqueness check + retry loop

The data model is one table; the real choice is the database

Asked at Amazon — Design a URL shortening platform Amazon's version is the whole platform at once. You are asked how short codes get made and what happens when two collide, how users claim custom aliases, when links expire, and what kind of store can hold billions of mappings when nearly all traffic is clicks rather than creates. Abuse handling, rate limits, and click analytics are part of the ask from the start rather than stretch goals.

The schema that serves Amazon's "billions of links" is almost embarrassingly small:

CREATE TABLE links (
    code        VARCHAR(10) PRIMARY KEY,  -- base62 short code, or custom alias
    long_url    TEXT        NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at  TIMESTAMPTZ,              -- NULL = never expires
    owner_id    BIGINT
);

Every hot query is a point lookup by primary key: given code, return long_url and expires_at. No joins on the read path, no range scans except a background expiry sweep. That access pattern is exactly what key-value stores are built for, which is why DynamoDB or Cassandra with code as the partition key is the textbook pick: single-digit-millisecond point reads that scale horizontally without you managing the sharding. If you go this route, be ready to talk partition-key design; our DynamoDB data modeling guide covers the trade-offs interviewers push on.

Postgres is not a wrong answer at 6 billion rows — hash-partition on code and it holds up. The honest framing is that a relational database buys you transactions and ad-hoc queries you will never use here, at the cost of harder horizontal scaling. Saying that out loud beats pretending one option is free.

Custom aliases fold into the same table: an alias is just a code the user chose, and the primary key enforces uniqueness for both kinds. The details worth volunteering are a reserved-word list (api, admin, login), a length floor so aliases cannot squat the auto-generated namespace, and a decision on case sensitivity. Base62 is case-sensitive, and links get read aloud over the phone, which is a real product argument for a case-insensitive alias tier even though it shrinks the space.

The classic time-sink here is normalizing into a third-normal-form constellation of users, links, domains, and clicks. Ten minutes gone, and the redirect path — the part being graded — still undiscussed.

The read path decides your grade: 301 vs 302, then the hot key

Asked at Microsoft — Design a scalable URL shortener A structured take for a data engineering role: cover the create and resolve APIs, the data model, short-code generation and uniqueness, then caching, scaling, and operational concerns, roughly in that order. The prompt is a checklist, and caching gets its own line: a strong hint about where the interviewer plans to spend the back half of the round.

Start with the HTTP status code, because it is the trade-off candidates most often get backwards.

A 301 Moved Permanently invites browsers and intermediary caches to remember the redirect, often indefinitely. Repeat clicks never touch your servers again. That sounds like free capacity until you enumerate what you gave up: you cannot count those clicks, you cannot change where the link points, and if the destination turns out to be a phishing page, you cannot kill it for anyone whose browser already cached the 301. A 302 (or 307 to preserve the request method) keeps every click flowing through your origin: full analytics and revocable links. You buy back the extra load with your own caching tiers. For a commercial shortener whose product is the click data, 302 is the defensible default. If the interviewer pushes back on load, that is your cue to walk the caching hierarchy, not to switch to 301.

url shortener system design

Two properties make this path unusually cache-friendly, and naming them is senior signal. First, the mapping is immutable: in the base design a code never re-points to a different URL, so invalidation, the hard half of caching, nearly disappears. Only deletion and expiry can stale a cache entry, which bounds your Redis TTL. Note what this rules out: if you offer link editing as a product feature, edits become a third staleness source and every cache tier needs active invalidation. That is exactly why the base design keeps mappings immutable and spends the 302's flexibility on revocation, not re-pointing — kill a bad link by deleting it, never by editing it in place. Second, traffic is violently skewed: a small fraction of links absorbs most reads.

That skew is also the section's named failure mode: the hot key. One link goes viral and produces hundreds of thousands of reads per second on a single code — enough to saturate the one Redis shard that key hashes to, no matter how many shards you have. The fix is the small in-process LRU in the diagram: each redirect node caches its own hottest few thousand codes in local memory, so the viral key is answered at every node without any network hop. A viral link is as immutable as any other, so the only cost is a slightly stale expiry check, bounded by the local TTL.

Two more read-path details close out Coinbase's requirements from earlier. Expiry is enforced on read: compare expires_at against now before redirecting. A background sweep deletes expired rows lazily, but enforcement never depends on the sweep having run. And click logging is fire-and-forget onto a queue. The moment an analytics write sits synchronously on the redirect path, your p99 belongs to your slowest analytics consumer.

Scaling writes: hand out ID blocks, do not coordinate per request

Asked at Remitly — Design a Production-Ready URL Shortener The "production-ready" framing is the tell. Beyond a create-resolve-manage API and a schema, you are expected to explain how IDs get generated without ever colliding, then keep going into territory a toy version skips: how the data is partitioned and replicated, what consistency you promise, how you throttle abuse and spam, and where click tracking lives.

The counter approach from the generation section has one obvious flaw at scale: a single global counter is a single point of contention and failure. Every create, everywhere, serializes through it. The fix is coarse coordination: allocate in blocks, not per request.

url shortener system design

Each write node asks the allocator for a range of 100,000 IDs, then serves creates from local memory with no coordination at all until the block runs dry. The allocator itself can be a single database row updated with an atomic increment, or a ZooKeeper counter; it is touched once per 100,000 creates, so it never becomes the bottleneck. When a node crashes mid-block, the unused IDs are simply lost. Against a 62^7 namespace of 3.5 trillion, discarding even millions of IDs per year is a rounding error, and saying "gaps are free, so I will not build recovery for them" is exactly the kind of cost-benefit judgment this round exists to surface.

Two alternatives are worth naming so the interviewer knows you rejected them deliberately. A pre-generated key table (the classic KGS design) stores unused codes and marks them consumed. It works, but you now manage used/unused state transitions and a second table for what block allocation does with one integer. Snowflake-style IDs embed a timestamp and machine ID in 64 bits, which base62-encodes to 11 characters: meaningfully longer links, justified only if you independently need time-ordered IDs.

The consistency question hides a nice failure mode: a user creates a link and clicks it one second later. If reads go to a replica that has not caught up, the brand-new link 404s — a terrible first impression. Cheapest fix: on create, write the mapping through to the cache before returning, so the immediate read never touches a replica at all.

The scaled-down round: when the interviewer wants a hashmap, not Kafka

Asked at Shopify — Build a Local URL Shortener A senior SDE round: build a tiny URL shortener that runs locally. The discussion covered input validation, MD5-based encoding and what to do about collisions, and where to store the mapping: the candidate floated Redis and a small database and the interviewer rejected both as unnecessary, steering toward an in-memory map serialized to a JSON file. A back-of-envelope memory estimate near 1 TB prompted a partitioning suggestion, which also got waved off. Tests were expected to hit 90%+ line and branch coverage.

This round is the distributed-systems answer's mirror image, and it trips up exactly the candidates who prepared hardest. The interviewer turning down both Redis and a small database is not an obstacle — it is the rubric telling you what round you are in. Shopify wanted a well-engineered small program: validated inputs, an honest collision story, clean serialization, and a test suite that actually exercises the branches. Reciting the CDN tier here reads as not listening.

The skills being graded are low-level design skills: API shape, error handling, class boundaries, testability. They are worth preparing separately; our low-level design interview guide covers that rubric in depth. The J.P. Morgan group-onsite version sits between the extremes: a 45-minute system design slot inside a three-and-a-half-hour loop, where time-boxing means you lead with requirements, generation, and the read path, and let analytics wait to be asked for.

One habit serves every variant: before drawing anything, say what you are optimizing for and confirm it. "I am assuming read-heavy at scale — is that the version you want, or should we keep this in one process?" costs fifteen seconds and buys you the entire round.

Video: Tiny URL - System Design Interview Question (URL shortener)