Skip to content
Jarviix

Tech · 7 min read

Designing a URL Shortener: An Interview-Style Walkthrough

A complete walkthrough of designing a URL shortener at interview depth — requirements, ID generation, storage, caching, scaling, and the trade-offs at every step.

By Jarviix Engineering · Apr 19, 2026

Smartphone displaying programming code on a screen
Photo via Unsplash

The URL shortener is one of the classic system design interview prompts because it's deceptively rich. The basic idea fits in two sentences; doing it well at scale touches identifier design, storage, caching, analytics, and operational resilience.

This post walks through the full design end-to-end — the way you'd actually build it.

Step 1: Requirements

Always start here. Most candidates skip and end up designing the wrong thing.

Functional

  • Given a long URL, produce a short URL (e.g., https://j.ix/abc123).
  • Given a short URL, redirect to the long URL.
  • Optionally: custom aliases (https://j.ix/launch).
  • Optionally: expiration.
  • Optionally: per-link analytics (clicks, geo, referrer).

Non-functional

  • Read-heavy. ~100:1 reads to writes is typical.
  • Redirects must be fast — aim for <100ms p99.
  • Highly available. A short URL going down is a public embarrassment.
  • Durable. Once we've handed out a short code, it must never resolve to a different URL.

Scale assumptions (for sizing)

Make explicit:

  • 100M new URLs per month → ~40 writes/second average, peak ~100/sec.
  • 100:1 read ratio → ~4000 reads/second average, peak ~10K/sec.
  • Average URL length 100 bytes; over 5 years that's ~6B URLs total.

Step 2: API design

Two endpoints, plus admin for analytics:

POST /shorten
Content-Type: application/json
Authorization: Bearer ...
{
  "url": "https://example.com/some/long/path",
  "custom_alias": "launch",       // optional
  "expires_at": "2026-12-31T..."  // optional
}
→ 201 Created
{ "short_url": "https://j.ix/abc123", "code": "abc123" }

GET /:code
→ 301 Moved Permanently
Location: https://example.com/some/long/path

301 vs 302: 301 is permanent (browsers cache aggressively). For analytics, 302 makes every click hit your server again. Most production shorteners use 302 for the analytics value; 301 if click counts don't matter.

Step 3: ID generation

The core design decision. Three approaches:

Approach A: Hash the URL

code = base62(sha256(url)[:7])

Pros. Same URL → same code (idempotent). No ID coordination needed.

Cons. Collisions on 7 characters of base62 (~3.5T values) start mattering at billions of URLs. You need a collision-handling path (rehash with salt, bump length).

Approach B: Base62-encode an auto-incrementing ID

Each new URL gets the next integer ID; encode it in base62 to get a short code.

ID 1 → "1"
ID 100 → "1C"
ID 1,000,000 → "4c92"
ID 6,000,000,000 → "6t8GXa" (6 chars)

Pros. Shortest possible URLs. No collisions ever. Storage is trivial (just store the long URL by ID).

Cons. IDs leak rate (someone sees code=1000 then code=2000 an hour later → "they get ~1000/hour"). IDs are sequential, so guessable.

To remove the guessability, encrypt or scramble the ID before encoding (e.g., a small Feistel cipher). The output is still short and unique but no longer ordered.

Approach C: Random ID with collision check

Generate a random 7-char base62 code; check if it exists; if so, retry.

Pros. Codes are unguessable. Simple.

Cons. Slightly more complex write path (insert with unique constraint, retry on conflict).

For an interview answer, Approach B with a scrambling step (or Approach C with collision retry) are both defensible. I'd reach for B at very high write rates (no DB roundtrip for collision check), and C for moderate scale where simplicity wins.

Distributed ID generation

If using auto-increment IDs and you have many write nodes, you need either:

  • A central ID service (single point of contention, but trivial).
  • A range-based scheme (each node gets a block of IDs to hand out, refills when low).
  • Snowflake-style IDs (each node uses time + node ID + sequence to generate unique IDs without coordination).

Twitter Snowflake produces 64-bit IDs; truncate or scramble for shorter codes if needed.

Step 4: Storage

The schema is one table:

urls (
  code        VARCHAR(8) PRIMARY KEY,
  long_url    TEXT NOT NULL,
  user_id     BIGINT,
  created_at  TIMESTAMP DEFAULT NOW(),
  expires_at  TIMESTAMP,
  click_count BIGINT DEFAULT 0
)

Index on code (it's the primary key — done). Optional secondary index on user_id if you want "list links by user".

Choosing the database. This is a key-value access pattern at scale; DynamoDB, Cassandra, or any KV store fit naturally. Postgres is fine through hundreds of GB. The choice rarely makes or breaks the design — both work.

Sharding. Once you're past one machine's capacity, shard by code. Hash sharding works because reads are point lookups by primary key; no cross-shard queries needed.

Step 5: Caching

Reads are the hot path. Cache aggressively.

  • CDN. Set Cache-Control: public, max-age=86400 on the redirect response. Most popular short URLs become CDN-cached and never hit your origin.
  • Redis. code → long_url mapping for the hot keys. ~1M most-recent codes, ~hours TTL. Sub-millisecond per lookup.
  • In-process. Per-instance LRU cache for ultra-hot codes. ~100K entries. Saves the Redis round trip.

A typical hit-rate cascade: 70% CDN, 80% Redis on the rest, 95% origin in-process on the rest → ~0.3% of redirects hit the database.

Step 6: Analytics (without slowing down redirects)

You want to count clicks but not pay for that on the request path.

Pattern:

  1. Redirect immediately (no DB write).
  2. Asynchronously emit an event to Kafka/Kinesis: {code, timestamp, ip, user_agent}.
  3. A separate consumer aggregates these and increments click_count periodically (every minute, every hour, batch).

If you absolutely need real-time counts, a Redis INCR is fast enough to do inline:

redis.incr(f"clicks:{code}")
# periodic job flushes Redis counts to the DB

Either way, the redirect itself never does a DB write.

Step 7: Custom aliases

Slightly different write path: insert into the urls table with the user-provided code, return 409 Conflict if it exists.

Reserve a namespace of disallowed aliases (api, health, login, admin, anything you might want for product paths later).

Step 8: Expiration

Either:

  • A nightly batch job that deletes rows where expires_at < NOW().
  • A TTL field in the cache so expired URLs naturally fall out of the hot path.
  • For DynamoDB / Cassandra: set the TTL on the row directly; the database expires it automatically.

The redirect handler also checks expires_at and returns 410 Gone if the URL has expired but hasn't been cleaned yet.

Step 9: Operational hygiene

A few things that turn a working design into a deployable one:

  • Bot filtering. Block known scraper / link-checker user agents from inflating click counts.
  • URL safety. Run new long URLs through Google Safe Browsing (or equivalent). Block or warn on phishing/malware URLs. Otherwise your shortener becomes a launder for malicious links.
  • Rate limiting. Per-user write limits to prevent abuse. Anonymous shorteners are abuse magnets.
  • Auth on the create path. Even if writes are public, require API keys. Lets you cut off abuse without a global outage.
  • Backups. The mapping is the entire product; daily backups, periodic restore drills.

Step 10: Failure modes to think about

  • Database down. Cache absorbs reads for hours. Writes fail; OK to surface 503.
  • Redis down. Reads fall through to DB. DB needs the headroom; this is why you load-test with Redis disabled.
  • Hot key. A single short URL goes viral. Already absorbed by CDN; if you don't have CDN, Redis does it.
  • Incorrect long URL. Bad write means a permanent wrong redirect. Validation matters; immutability matters.

What this exercise teaches

The URL shortener is a great interview problem because it covers most of the system design surface in one prompt:

  • ID generation strategies and their trade-offs.
  • KV access patterns at scale.
  • Cache hierarchies.
  • Asynchronous analytics paths.
  • Failure mode reasoning.

Get good at this one and you'll hit most of these themes in any system design question that follows.

The patterns here repeat across many systems. Caching strategies, load balancing, and sharding and partitioning are the deeper dives on three of the core building blocks. The URL shortener HLD writeup and LLD writeup take this same problem to interview depth — capacity math, schema, ID generation, and the object model — if you want the formal version.

Frequently asked questions

Is base62 or hashing better for short codes?

Base62 of an auto-incrementing ID is simpler and gives the shortest possible URLs. Hashing (with a collision check) is useful when you don't want the ID to leak ordering information, or when you need URL stability for the same input.

Do I need a relational DB?

Either works. The access pattern is dominantly key-value (lookup by short code), so DynamoDB / Cassandra fit naturally. A relational DB is fine and arguably simpler to operate at small-to-medium scale.

How do I handle expiration and analytics?

Expiration: store an expires_at column and periodically clean up. Analytics: write to an asynchronous event stream (Kafka, Kinesis) on each redirect; aggregate offline. Don't make every redirect block on a metrics write.

Related Jarviix tools

Read paired with the calculator that does the math.

Read next