Skip to content
Backend & systems

Design a URL Shortener: A System Design Interview Walkthrough

A URL shortener is a favorite warm-up because the happy path fits on a napkin and every follow-up exposes a real trade-off: how codes are generated, which redirect status to send, how to count clicks without slowing redirects, and how to stop the service from becoming a phishing relay. This walkthrough follows a 45-minute interview order, labels every estimate as an assumption, and includes a runnable base62 encoder with its exact output.

By Published

8 min readEditorial analysisUpdated
  • system design
  • URL shortener
  • HTTP
  • caching
  • interview practice
What to remember

Keep the redirect path to one cache lookup with a database fallback, and move everything else, such as counting clicks, off it. Choose code generation by what must never happen, collisions or guessable codes, and choose 301 or 302 by whether you need to see every click.

Requirements and labeled estimates

Functional requirements: create a short link for a long URL, optionally with a custom alias and an expiry; redirect a short code to its destination; show basic click counts to the owner. Non-functional requirements: redirects must be fast and highly available, codes should not be trivially enumerable if links can be private, and a destination changes only when its owner edits it. Confirm which of these the interviewer actually wants before drawing anything.

Then estimate, and say out loud which numbers are assumptions. The table assumes 100 million new links per month, a 100:1 ratio of redirects to creations, five-year retention, and about 500 bytes per stored link. That gives roughly 39 writes and 3,900 redirects per second on average; peaks are higher, so state the multiplier you plan for. Six billion links fit comfortably in seven base62 characters, which matters later in the ID discussion.

Back-of-the-envelope estimates (assumed inputs)
QuantityValueDerivation
New links100M per monthAssumption
Writes≈39 per second100M ÷ (30 × 86,400)
Redirects≈3,900 per second100 × writes (assumed ratio)
Links after 5 years6 billion100M × 60 months
Storage≈3 TB6B × 500 bytes (assumed)
7-char space used≈0.17%6B ÷ 62^7

API and data model

Keep the API small: POST /api/links with the long URL and an optional alias and expiry returns 201 with the code; GET /{code} redirects; owners can PATCH or DELETE their links and read GET /api/links/{code}/stats. Validate input at creation: allow only the http and https schemes, cap the length, and reject your own domain to avoid redirect loops. Base62 codes use only letters and digits, which RFC 3986 lists as unreserved characters, so they never need percent-encoding.

The core table is links(code primary key, long_url, owner_id, created_at, expires_at, status). Redirects are point lookups by code, a key-value access pattern that any store with fast primary-key reads can serve, and one that shards cleanly by code if a single node is not enough. Custom aliases share the code namespace, so reserve words like api and admin, and enforce uniqueness with the insert itself, insert-if-absent on the primary key, rather than a check followed by an insert.

Generate codes: counter, hash, or random

A counter encoded in base62 is unique by construction and produces the shortest codes. Its problems are coordination and guessability. Coordination is solved with blocks: each API instance reserves a range, say 1,000 IDs, and hands them out locally. PostgreSQL sequences do the same with CACHE and document the side effect: unused values are lost, so gaps appear. Guessability is real, since consecutive IDs give 4c92 and 4c93, and multiplying by a constant does not hide the order, because neighbors still differ by that constant.

Hashing the long URL and keeping the first seven base62 characters gives the same code for the same URL, but truncation means collisions: on a clash with a different URL, rehash with a salt and retry. Random seven-character codes avoid ordering entirely; insert with a uniqueness check and retry on conflict. The collision chance per insert equals the fraction of the space already used, about 0.17% at six billion links. A pre-generated key pool does that work ahead of time, at the cost of another stateful service.

  • Counter + base62: no collisions and the shortest codes, but enumerable.
  • Hash + truncate: identical URLs share a code; collisions need retries.
  • Random + unique insert: not enumerable; retries grow as the space fills.
  • Pre-generated pool: fast handout, plus a service to run and back up.

Base62 encoding you can run

The encoder repeatedly divides by 62 and maps each remainder to a character; the decoder reverses it with place values. The alphabet order here is digits, then lowercase, then uppercase. Any fixed order works, but it can never change after launch, because every existing code would then point somewhere else. An invalid character raises KeyError, which the redirect handler can map straight to 404 without touching storage.

Python 3.12 printed: 0 -> 0, 61 -> Z, 62 -> 10, 1,000,000 -> 4c92, 1,000,001 -> 4c93, and 3,521,614,606,207 -> ZZZZZZZ, followed by 7-char codes: 3,521,614,606,208. The 62 -> 10 line is place value at work, just like 10 in decimal. The largest seven-character code is 62^7 − 1, so seven characters cover about 3.5 trillion IDs, and the 4c92 to 4c93 pair shows why raw counters are guessable.

ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
INDEX = {ch: i for i, ch in enumerate(ALPHABET)}


def encode(n: int) -> str:
    if n < 0:
        raise ValueError("ids are non-negative")
    out = ""
    while True:
        n, r = divmod(n, 62)
        out = ALPHABET[r] + out
        if n == 0:
            return out


def decode(code: str) -> int:
    n = 0
    for ch in code:
        n = n * 62 + INDEX[ch]  # KeyError: not a valid code
    return n


for n in (0, 61, 62, 1_000_000, 1_000_001, 62**7 - 1):
    code = encode(n)
    assert decode(code) == n
    print(f"{n:>17,} -> {code}")
print(f"7-char codes: {62**7:,}")

301, 302, 307 or 308: pick the redirect deliberately

RFC 9110 defines 301 and 308 as permanent and 302 and 307 as temporary. For historical reasons a client may change POST to GET after a 301 or 302, while 307 and 308 forbid changing the method. Shortener redirects are GET requests, so method handling rarely matters. Caching does: 301 and 308 are heuristically cacheable, so when the response carries no explicit expiration, RFC 9111 lets a cache choose its own freshness lifetime. It prescribes no algorithm; it only suggests that, when Last-Modified is present, the lifetime stay below a fraction of the time since then, typically 10%.

That is the trade-off. A permanent, cached redirect saves round trips for repeat visitors, but those repeat clicks never reach you, so analytics undercount them, and a cached destination cannot be changed reliably. A 302 is not heuristically cacheable, so unless you send Cache-Control or Expires headers, every click arrives and the owner can edit the link. A defensible default is 302 for editable or tracked links, and 301 or 308 with an explicit Cache-Control max-age only for immutable links whose click counts do not matter.

Redirect status codes for a shortener (RFC 9110)
StatusPermanentMethod on redirectHeuristically cacheable
301YesMay change POST to GETYes
302NoMay change POST to GETNo
307NoMust not changeNo
308YesMust not changeYes

Caching, analytics, and abuse prevention

Redirect traffic is read-heavy, so put a cache-aside layer keyed by code in front of the database, with a TTL, and briefly cache misses for unknown codes so scanners cannot hammer storage. Never update a counter row synchronously on each click. Decide the redirect first, then publish a click event with the code, time, referrer and coarse user-agent data to a queue, and aggregate downstream. Queue delivery is often at least once, so either deduplicate by event ID or accept approximate counts.

A shortener hides destinations, which makes it attractive for phishing. Rate limit link creation per account and per IP, check destinations against a threat list, and re-check periodically, since a page can turn malicious after creation. Google's Safe Browsing API checks URLs against lists of unsafe sites, but as of November 2026 Google says that API is for non-commercial use only and directs commercial, meaning revenue-generating, uses to its Web Risk API. Keep a status column so a reported link can be disabled immediately.

URL shortener architecture: client, shortener API, ID allocator, cache, database, queue and analytics store, with the redirect path and the asynchronous click path
A redirect needs only the cache, plus the database on a miss. Click events leave through a queue, so a slow analytics store never delays a redirect. RecallDeck diagram

Run the interview in 45 minutes

A workable time plan: five minutes on requirements and estimates, five on the API and schema, ten on code generation, ten on the read path and redirect semantics, ten on scale and failure, such as a cache outage, a database failover, or a link that suddenly goes viral, and five on abuse and analytics. Interviewers usually pick one deep dive, and code generation and redirect semantics are natural candidates, so have numbers ready for both.

Rehearse by changing one assumption and redoing the arithmetic: ten times more links, or six-character codes. With six billion links, six characters would leave the space about 10.6% full, so roughly one random insert in ten would collide and retry. The articles below cover the cache behavior behind the read path and a structured way to practice complete designs.

Quick answers

Frequently asked questions

Should the same long URL always get the same short code?

It is a product decision. Deduplication saves storage, but it merges click statistics across users and prevents per-owner settings such as expiry. A middle ground is deduplicating per owner, so one user creating the same link twice gets one code.

What happens to redirects if the cache fails?

Every lookup falls through to the database, which must absorb the full redirect rate or shed load. Size the database for that case, coalesce concurrent misses for the same code into one query, and alert on the cache hit ratio.

How do you expire links?

Store expires_at, check it on every lookup, and return 404 or 410 Gone after expiry. A background job can delete or archive expired rows later; the read-time check keeps behavior correct even before cleanup runs.

Do custom aliases need special handling?

Yes. They share the namespace with generated codes, so reserve system words, block offensive or misleading strings, and enforce uniqueness at insert time. A separate length rule, such as aliases of at least eight characters while generated codes stay at seven, keeps the two from ever colliding.

Source notes

References and review policy

Information checked on November 7, 2026. Section links identify sources for factual claims and technical explanations. Interpretations, practice scenarios and preparation recommendations are RecallDeck’s editorial work.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

Interview strategy3 min

How to Prepare for a System Design Interview

Prepare for a system design interview with a repeatable framework for requirements, estimates, architecture, data, failure, and trade-offs.

2 quick answers