How to Design a URL Shortener Like TinyURL — Step-by-Step for System Design Interviews

A few weeks ago, a friend of mine — three years into his backend career, preparing hard for an SDE role at a company most of us would kill to get into – called me in a slight panic.

“They asked me to How to Design a URL Shortener Like TinyURL,” he said. “I’ve used it a hundred times. I froze.”

That’s the thing about this question. It sounds trivial. Paste a long link, get a short one back. What’s there to design? But that’s exactly why interviewers love it — it’s simple enough to explain in one sentence, and deep enough to fill a 45-minute interview if you know where to dig.

This is the walkthrough I wish someone had given him — not a dry spec sheet, but the story of what’s actually being tested at each step, and where most candidates quietly lose points.

Nailing the Requirements

Every senior candidate makes the same first move: they don’t touch the whiteboard until the requirements are locked in. A design built on the wrong assumptions falls apart no matter how elegant it looks.

Functional requirements — what it must do

  • Shorten it. Submit a long URL, get back a short, unique alias.
  • Redirect it. Visit the short URL, land on the original long URL.

Non-functional requirements — what it must be

  • Highly available. If this goes down, every short link pointing through it breaks at once — a very public failure.
  • Scalable. Traffic spikes hard when a single link goes viral.
  • Low latency on redirects. Nobody notices a URL shortener that works. Everybody notices one that takes three seconds.
Interview tip — always ask whether custom aliases, expiry, and click analytics are in scope. Bolting them on later, unasked, is how "below the line" features quietly become feedback against you.

Back-of-the-Envelope Estimation

This is the part candidates rush — and the part that silently justifies everything that follows. Skip it, and your caching strategy looks like a guess instead of a decision.

  • Assume 100 million new short URLs per month.
  • Over 5 years: 100M × 12 × 5 = 6 billion records.
  • At ~500 bytes/record: 6B × 500B ≈ 3 TB total storage. Very manageable.
  • Read:write ratio ≈ 100:1 — URL shorteners are read-heavy by nature.
  • 200M writes/month → 200M × 100 = 20 billion reads/month.

That last number is the whole ballgame. Twenty billion reads a month is what forces caching, read replicas, and horizontal scaling into the conversation — not because “caching is best practice,” but because the math demands it. Say that out loud; it shows reasoning, not memorization.

API Design

Create a short URL
POST
POST /api/v1/shorten
Content-Type: application/json
 
{
  "long_url": "https://example.com/very/long/path/to/resource",
  "custom_alias": "my-link",          // optional
  "expires_at":  "2026-12-31T00:00:00Z" // optional
}
201 Created
{
  "short_url":  "https://tinyurl.com/abc123",
  "short_code": "abc123",
  "expires_at": "2026-12-31T00:00:00Z"
}
StatusMeaning
400Bad Request — invalid URL format
409Conflict — custom alias already taken
429Too Many Requests — rate limit exceeded

Redirect to the original URL

GET
GET /abc123
302 Found
Location: https://example.com/very/long/path/to/resource
StatusMeaning
404Not Found — short code doesn’t exist
410Gone — link has expired

Why 302, not 301?

A favorite interviewer follow-up. 301 (Permanent) gets cached by the browser forever — fast, but you lose the ability to track clicks or change the destination. 302 (Temporary) means the browser asks your server every time — slightly more load, but you keep click analytics and the ability to kill or redirect a link at any time. Most production shorteners use 302 for exactly this reason.

Designing the Database Schema

Before drawing boxes and arrows, nail down what’s actually being stored — interviewers often ask you to sketch this explicitly, and it directly feeds the SQL-vs-NoSQL question later.

Core table — urls

ColumnTypeNotes
idBIGINT (PK)Snowflake-generated unique ID — the value that gets Base62-encoded into the short code
short_codeVARCHAR(10)Unique index. Base62 string or custom alias — the lookup key on every redirect
long_urlVARCHAR(2048)The original URL
is_custom_aliasBOOLEANUser-chosen vs. system-generated
user_idBIGINT, nullOnly needed if accounts/ownership are in scope
created_atTIMESTAMP
expires_atTIMESTAMP, nullDrives the 410 check on redirect
The single most important decision here: short_code needs a unique index. Every redirect does an exact-match lookup on it, and the same index is what enforces "custom alias already taken" at the database level — cheaper and safer than a check-then-insert in application code.

Optional — click_events (only if analytics is in scope)

ColumnTypeNotes
idBIGINT (PK)
short_codeVARCHAR(10)Indexed, references urls.short_code
clicked_atTIMESTAMP
referrerVARCHAR(255), null
geoVARCHAR(100), null

Keeping this as a separate table — fed asynchronously via a queue, as covered in the scenario questions below — means the hot redirect path never writes to it synchronously.

urls idBIGINT PK short_codeVARCHAR(10) UQ long_urlVARCHAR(2048) is_custom_aliasBOOLEAN user_idBIGINT, null created_atTIMESTAMP expires_atTIMESTAMP, null 1 : N (async) click_events (optional) idBIGINT PK short_codeindexed FK clicked_at, referrer, geo
FIG. 00 — DATA MODEL

SQL or NoSQL for urls?

Either is defensible in an interview, but the reasoning matters more than the pick. NoSQL (DynamoDB, Cassandra) is favored when the priority is horizontal write scale and built-in replication at billions of rows — short_code becomes the partition key, giving O(1) lookups since the schema has no relationships to speak of. SQL is favored when you want a database-enforced unique constraint on short_code and simple ACID guarantees around alias creation, at the cost of planning sharding yourself as the table grows.

High-Level Architecture

Here’s how a request actually moves through the system – the write path in amber, the read path in teal, kept deliberately separate because writes are rare and reads are relentless.

CLIENT LOAD BALANCER rate limit + validation WRITE SERVICE READ SERVICE ID GENERATOR snowflake id → base62 REDIS CACHE LRU · TTL · cluster HIT PRIMARY DATABASE short_code ↔ long_url miss READ REPLICA replicated copy replication 201 → short_url returned 302 → redirect (or 404 / 410)
FIG. 01 — HIGH-LEVEL ARCHITECTURE write path   read path

The write path: load balancer → write service → ID generator (unique numeric ID → Base62 short code) → primary database → short URL returned.

The read path: load balancer → read service → Redis. Cache hit → redirect immediately, sub-10ms. Cache miss → read replica (never the primary — that’s asking for a bottleneck) → check expiry (410 if expired, queued for async cleanup) or missing (404) → on success, write back to Redis with a TTL, then return 302.

That write-back-on-miss step is what keeps the cache warm without pre-populating it: popular links stay fast, forgotten ones simply age out.

How Do We Actually Generate the Short Code?

This is where an interviewer usually leans forward — there’s no single right answer, only trade-offs.

  • MD5 / hash-based — simple, but collisions are a real risk at billions of records; needs a resolution strategy.
  • Random string — generate, check DB for collision, retry — but that check-then-retry loop adds latency and doesn’t parallelize cleanly.
  • Base62(unique ID) — the common answer. Generate a globally unique, ordered ID (often via Snowflake: timestamp + machine ID + sequence, 64 bits), then encode it with the 62 characters [0-9, a-z, A-Z].
NEW LONG URL write request SNOWFLAKE ID timestamp | machine | seq 64-bit unsigned int BASE62 ENCODE [0-9 a-z A-Z] abc123
FIG. 02 — SHORT CODE GENERATION PIPELINE

Base62-over-Snowflake wins most interviews because it sidesteps collisions entirely — the ID is already unique before it’s ever encoded — and needs no coordination lookup before writing, which is exactly what a distributed, high-write system wants.

Caching — Where the Real Scale Lives

Remember the 20-billion-reads-a-month number? This is where it gets absorbed.

GET /abc123 REDIS CHECK LRU + TTL cluster hit 302 in <10ms miss READ REPLICA query long_url + expiry found → cache + 302 not found → 404 / 410
FIG. 03 — REDIRECT DECISION FLOW
  • Redis as key-value store: short_code → long_url.
  • LRU eviction — rarely-clicked links get pushed out first.
  • TTL per entry — stale or expired links don’t linger.
  • Clustering across nodes for capacity and fault tolerance — no single box holding it all.
A useful mental check: if a redirect resolves in under ~10ms, that's almost certainly a cache hit. Noticeably slower means a cache miss fell through to the database — worth mentioning when discussing observability.

Scenario-Based Interview Questions (Actually Asked at FAANG/MAANG)

Reading the design is one thing. Defending it under follow-ups is where interviews are won or lost. Here’s how to think through the ones that come up most.

Q1  Two users submit URLs at the exact same millisecond and collide on the same short code. What now?

Tests whether you understand why Base62-over-Snowflake avoids this at the source — uniqueness comes from ID generation, not a database-level check. With hashing instead, the right answer is retry-with-suffix backed by a DB uniqueness constraint, with the added latency acknowledged.

Q2  A single link gets 5 million clicks in an hour after going viral. Walk me through it.

The hot key problem. A naive cache can still get hammered on the same node, or the same DB row if TTL expires mid-spike. Strong answers mention extending TTL for popular keys, replicating a hot key across cache nodes, or a CDN layer for extremely popular redirects.

Q3  Two simultaneous requests for the same custom alias. How do you guarantee only one wins?

Tests whether you reach for a unique constraint on the alias column plus a transaction, instead of a “check-then-insert” pattern in application code — which has an obvious race condition between the check and the insert.

Q4  SQL or NoSQL for the core mapping table — and why?

No single right answer, but you’re expected to reason it through. NoSQL (DynamoDB, Cassandra) is often favored for horizontal write scale and built-in replication at this volume; SQL wins if you need strong transactional guarantees around alias uniqueness. The trade-off reasoning is what’s scored.

Q5  Redis restarts and the cache is completely empty. What happens, and how do you prevent a meltdown?

The cold cache / thundering herd problem — every read falls through to the DB at once. Good answers mention gradual cache warming, request coalescing (one request per key hits the DB while others wait on that result), and read replicas absorbing the spike.

Q6  How would you add click analytics — count, geography, referrer — without slowing the redirect down?

Tests whether you keep the hot redirect path untouched and instead fire-and-forget an event to a queue (Kafka), feeding a separate analytics pipeline — never writing analytics synchronously inside the read path.

Q7  How do you actually invalidate expired links at scale, across millions of rows?

Tests whether you reach for an async cleanup job (cron sweep or queue-based deferred deletion) rather than checking expiry synchronously on every single read — and whether you remember to invalidate the cache entry too, not just the DB row.

Q8  How would you shard the database as it outgrows a single primary?

Tests sharding-key intuition — typically sharding by a hash of the short_code so lookups route deterministically, versus sharding by creation time, which would concentrate all recent (and hottest) traffic on a single shard.

Final Thoughts

The URL shortener question isn’t hard because the system is complex — it’s hard because it’s deceptively simple, and simplicity is exactly what tempts candidates into skipping the reasoning interviewers are listening for. Nail the requirements, justify your numbers, and defend every trade-off — hashing vs. Snowflake, SQL vs. NoSQL, 301 vs. 302 — and this stops being a question to fear and becomes one of the easiest wins in your loop.

Next up: a rate limiter or a distributed cache — both share a lot of DNA with what we just built here.

Got a scenario question from a real interview that isn't on this list? Drop it in the comments — the best ones get added.

Leave a Comment