Design a URL Shortener

Design a URL shortener step by step: requirements, scale, short-code generation, redirects, caching, analytics, and the trade-offs behind every major decision.

Page content

A URL shortener turns a long link into a short one and sends people to the original page when they open it.

Alice wants to share this:

https://www.example.com/blog/2026/08/how-we-scaled-the-home-timeline?utm_source=twitter

She would rather send:

https://short.ly/k9mX2pQ

The main design question is:

How do we create unique short codes quickly, and how do we redirect millions of clicks to the right long URL with very low latency?

We will start with the product, estimate the traffic, and model the data. Then we will compare ways to generate short codes and let the workload lead the architecture.

1. Clarify the problem

“Design a URL shortener” can mean a tiny personal tool or a Bitly-scale product. I would ask:

  • Who creates links: anyone, or only logged-in users?
  • Can users choose a custom alias such as /hiring?
  • Do links expire?
  • Do we need click analytics?
  • Should the same long URL always map to the same short code?
  • What is the expected traffic and how long do we keep links?

If the interviewer gives no extra constraints, I would state these assumptions:

Users              Logged in to create links; anyone can click
Custom aliases     Supported, unique, optional
Expiry             Optional; default is no expiry
Analytics          Click counts and coarse location, not a full analytics product
Same long URL      May produce a new short code each time unless the user asks to reuse
Retention          Keep mappings until expiry or deletion

This keeps the interview on the two hot paths: create and redirect. Custom aliases, expiry, and analytics can be added once those paths are solid.

2. Functional requirements

The system must:

  1. Accept a long URL and return a short URL.
  2. Redirect a short URL to the original long URL.
  3. Optionally accept a custom alias.
  4. Optionally accept an expiry time.
  5. Let the creator delete or disable a link.
  6. Record enough click data to show basic analytics.

The first version does not include:

  • a full dashboard product;
  • A/B tests or branded landing pages;
  • QR codes;
  • preview pages that warn about malware; or
  • user-facing search over all shortened links.

We will briefly explain how analytics and safety checks fit later.

3. Non-functional requirements

Redirects are the user-visible path. Creating a link can be a little slower.

RequirementTarget
Redirect latencyp99 below 50–100 ms at the edge
Create latencyp99 below 200 ms
Redirect availabilityApproximately 99.99%
DurabilityNever lose an acknowledged mapping
Create consistencyThe returned short code must resolve immediately
Analytics consistencyClick counts may lag by a few seconds
UniquenessTwo live links must not share a short code
ScalabilityHandle a small number of extremely popular links

Consistency is not one global setting:

Short code uniqueness     must be immediate
Redirect target           must be the latest durable mapping
Click counters            may lag
Cache of popular links    may be slightly stale if a link is deleted

A deleted or expired link should stop redirecting quickly. A click count that is two seconds behind is acceptable.

Edge cases to keep in mind

  • Two users create the same custom alias at the same time.
  • A client retries create after a timeout.
  • A viral link receives millions of redirects in a few minutes.
  • Someone shortens a malicious or very long URL.
  • A mapping is deleted while its cache entry is still warm.
  • Short codes are guessed or enumerated.
  • Redis or Kafka becomes unavailable.

4. Estimate the scale

Use round numbers. We need enough arithmetic to find the bottleneck.

Assume:

New short URLs per month              100 million
Read:write ratio                      100:1
Average long URL size                 500 bytes
Short code length                     7 characters
Peak multiplier                       10×

Write traffic

100M / (30 × 86,400)
≈ 40 creates/second average

Peak
≈ 400 creates/second

Redirect traffic

40 × 100
= 4,000 redirects/second average

Peak
≈ 40,000 redirects/second

The first useful conclusion is:

Redirects greatly outnumber creates.

The architecture should make the redirect path extremely cheap. Extra work during create is acceptable if it keeps redirects fast.

Storage

A mapping row is roughly:

short_code     7–20 bytes
long_url       ~500 bytes
metadata       owner, timestamps, expiry
≈ 0.5–1 KB
100M × 0.7 KB
≈ 70 GB/month
≈ 840 GB/year before replication and indexes

This is manageable with partitioned storage. The hard part is not storing 70 GB. It is serving 40,000 lookups per second, many of them for the same popular codes.

How many codes do we need?

If we use base62 (0-9, a-z, A-Z):

62^6  ≈ 56 billion
62^7  ≈ 3.5 trillion

Seven characters give a large enough space for this product with room to grow. Six characters can work, but collisions and enumeration become more annoying.

5. What are we mapping?

Alice creates a link. The system stores:

k9mX2pQ  →  https://www.example.com/blog/...

When Bob clicks https://short.ly/k9mX2pQ, the system looks up that code and sends him to the long URL.

Two operations dominate:

Create     long URL  → unique short code
Redirect   short code → long URL

Everything else — analytics, aliases, expiry — hangs off those two lookups.

6. APIs

Keep the API small.

Create a short URL

Request:

POST /v1/links
Authorization: Bearer <token>
Idempotency-Key: <uuid>

{
  "long_url": "https://www.example.com/blog/2026/08/how-we-scaled-the-home-timeline",
  "custom_alias": null,
  "expires_at": null
}

Response:

HTTP 201 Created

{
  "short_code": "k9mX2pQ",
  "short_url": "https://short.ly/k9mX2pQ",
  "long_url": "https://www.example.com/blog/2026/08/how-we-scaled-the-home-timeline"
}

The idempotency key matters. If Alice’s phone times out after the server saved the mapping, retrying should return the same short code, not create a second one.

Redirect

Request:

GET /k9mX2pQ

Response:

HTTP 302 Found
Location: https://www.example.com/blog/2026/08/how-we-scaled-the-home-timeline

Other endpoints

GET    /v1/links/{short_code}
DELETE /v1/links/{short_code}
GET    /v1/links/{short_code}/stats

Return 400 for an invalid URL or alias, 401 when create requires login, 404 when the code is unknown or expired, 409 when a custom alias is taken, and 429 when rate limits are exceeded.

7. Basic data model

We need one core record:

Link
----
short_code
long_url
owner_id
created_at
expires_at
is_deleted

The lookups are:

short_code → long URL          redirect path
owner_id   → that user's links optional dashboard

short_code is the primary key. Redirects never query by long URL on the hot path.

Click events are not stored on this row. If we incremented a counter inside the redirect request, the most popular links would turn one row into a write hotspot. Analytics belong on an asynchronous path.

Source of truth       Link records
Derived data          Click counts, daily stats
Cache                 short_code → long_url for hot links
Event system          Click events for analytics

8. The central decision: how do we generate short codes?

There are three common approaches.

1. Hash the long URL
2. Take a counter and encode it
3. Generate a random code and check uniqueness

None is free. The right choice depends on uniqueness, length, and whether the same long URL should reuse a code.

9. Approach 1: hash the long URL

long URL
SHA-256
take the first 42 bits
encode as base62
7-character code

Advantages

  • The same long URL always produces the same code.
  • No central counter.
  • Easy to explain.

Disadvantages

  • Different URLs can collide after truncation.
  • You cannot choose the length independently of collision risk.
  • Query strings and trailing slashes create different hashes for “the same” page.
  • Custom aliases do not fit this model.

Collision handling looks like this:

Compute candidate code
Insert into the store
Conflict?
   ├── no  → done
   └── yes → hash(url + salt) and retry

This works at low volume. At scale, retries and “is this the same URL or a true collision?” checks add complexity without buying much.

Hashing is a weak default for this interview unless the interviewer requires one short code per unique long URL.

10. Approach 2: counter plus base62

Keep a global counter. Each new link gets the next integer, then we encode it.

1        → 1
61       → z
62       → 10
12345    → 3d7

Base62 is a compact alphabet that stays URL-safe without extra encoding.

Advantages

  • Codes are unique if the counter is unique.
  • Codes stay short: the first millions of links use fewer than seven characters.
  • No collision retries on the common path.

Disadvantages

  • A single counter is a hotspot.
  • Sequential codes are easy to enumerate: /1, /2, /3.
  • Distributed services cannot all increment one database row at 400 writes per second without coordination.

The enumeration problem is real for a public shortener. Attackers can walk the code space and discover unpublished links.

We can still use counters if we issue ranges instead of one global lock:

ID service
Give app server the range 1,000,000–1,099,999
App server assigns IDs locally
Ask for the next range when it runs out

Redis INCR or a small ID service can play this role. The trade-off is an extra dependency and the need to handle a server crashing with unused IDs in memory. Unused IDs are fine; uniqueness still holds.

If we take this path, I would:

  • issue IDs in ranges;
  • encode with base62;
  • pad or hash-mix the value so codes are not obviously sequential; and
  • keep the ID service small and highly available.

11. Approach 3: random unique codes

Generate seven random base62 characters and insert.

Generate k9mX2pQ
INSERT ... WHERE short_code is unused
Success? return
Conflict? generate again

Advantages

  • No central counter.
  • Harder to enumerate than /1, /2, /3.
  • Custom aliases use the same uniqueness check.
  • Easy to run on many stateless servers.

Disadvantages

  • A uniqueness check is required.
  • Collision probability rises as the space fills, though 7 characters is vast at 100M links per month.
  • Randomness must be strong enough that codes are not predictable.

At our scale this is simple and strong. I would use it for auto-generated codes, with the database unique constraint as the source of truth.

If two requests insert the same random code, one succeeds and the other retries. That is rare.

Choice for this design: random 7-character base62 codes, uniqueness enforced by the Link store. Use the same insert path for custom aliases. If the interviewer wants denser codes and easier debugging, range-allocated counters are a valid alternative.

12. 301 or 302?

This looks like a small HTTP detail. It is a product decision.

301 Moved Permanently
Browser and some CDNs may cache the redirect
Later clicks may never hit our servers

302 Found
Browser asks us again on the next click
We see the traffic and can change or disable the target

If we need click analytics or the ability to disable a link, 302 is the better default. We can still cache the mapping inside our own Redis/CDN with a short TTL so the extra hop is cheap.

If links never change and analytics are optional, 301 reduces load. I would say that out loud, then pick 302 for this design because we assumed analytics and deletion.

A 307/308 discussion is a bonus: they preserve the request method. For ordinary GET clicks, 302 is enough.

13. High-level architecture

We now know the constraints: cheap redirects, unique codes, asynchronous analytics.

                         ┌──────────────┐
                         │   Clients    │
                         └──────┬───────┘
                         ┌──────▼───────┐
                         │ API Gateway  │
                         │ Auth + limits│
                         └───┬─────┬────┘
                             │     │
                    create   │     │  GET /{code}
                             ▼     ▼
                      ┌────────────┐   ┌────────────┐
                      │Link Service│   │Redirect Svc│
                      └─────┬──────┘   └─────┬──────┘
                            │                │
                      ┌─────▼──────┐   ┌─────▼──────┐
                      │ Link Store │   │   Cache    │
                      │ unique key │   │ code→URL   │
                      └────────────┘   └─────┬──────┘
                                             │ miss
                                       ┌─────▼──────┐
                                       │ Link Store │
                                       └────────────┘
                         Kafka
                    Analytics workers

Why each component exists

  • Link Service: validates URLs, generates codes, stores the mapping.
  • Redirect Service: answers the hot path with a cache-first lookup.
  • Link Store: source of truth for code → URL.
  • Cache: keeps popular mappings in memory.
  • Kafka: moves click events off the redirect path.
  • Analytics workers: aggregate counts without blocking users.

Create flow

  1. Authenticate and rate-limit.
  2. Validate the URL and optional alias.
  3. Generate a code or use the alias.
  4. Insert the mapping. On conflict, retry or return 409.
  5. Warm the cache.
  6. Return the short URL.

Redirect flow

  1. Look up the code in cache.
  2. On miss, read the Link Store and fill the cache.
  3. If missing, expired, or deleted, return 404.
  4. Return 302 with Location.
  5. Publish a click event asynchronously. Do not wait for analytics.

14. Caching

Most redirects hit a small set of popular codes. Cache them.

Key     link:{short_code}
Value   long_url, expiry, deleted flag
TTL     minutes to hours, jittered

Invalidation

Link created     write-through so the creator's first click hits
Link deleted     delete the cache key
Link expired     TTL can cover it; still check expires_at

On a cache miss, read the store. If the row is gone, cache a short negative entry so attackers scanning unknown codes do not beat on the database. Keep negative TTLs short.

Hot keys

A celebrity announcement can send millions of clicks to one code. One Redis key on one shard becomes the bottleneck.

Mitigations:

  • replicate the hot key on several cache nodes;
  • cache at the CDN/edge with a short TTL;
  • add local in-process LRU on redirect servers for the hottest codes;
  • coalesce store lookups so many misses become one database read.

Redis is a cache. If it disappears, redirects continue from the Link Store at higher latency and with tighter rate limits.

15. Storage choices

Redirects are a key lookup:

short_code → long_url

That access pattern fits a key-value or wide-column store well: Cassandra, DynamoDB, or a similar system with short_code as the partition key.

A sharded relational database also works at this scale if we already operate one. The unique constraint on short_code is then straightforward.

I would not introduce a graph database, search cluster, or object store for the mapping itself.

Partitioning

Link Store     partition by hash(short_code)
Analytics      partition click events by short_code and time bucket

Hashing the code spreads viral traffic better than range partitioning on a sequential counter, which is another reason random or mixed codes help.

16. Idempotency, uniqueness, and races

Two races matter.

Duplicate create from a retry

Alice’s client sends the same request twice.

Store Idempotency-Key → short_code
for 24 hours

The second request returns the original result.

Duplicate custom alias

Alice and Bob both request /hiring.

The unique index on short_code lets one insert win. The loser receives 409. Do not check-then-insert without a constraint; both checks can pass.

Random code conflicts use the same constraint. Retry with a new code a small number of times, then fail.

17. Analytics without slowing redirects

A click should not wait on a counter write.

Redirect
302 to the user
emit Clicked { code, time, region, user-agent }
Kafka
workers increment counts

Kafka (or a managed queue) exists here because:

  • redirect latency must not include analytics;
  • bursts from a viral link can be absorbed;
  • several consumers can compute counts, fraud scores, and reports;
  • events can be replayed if an aggregator fails.

If Kafka is down, still redirect. Buffer events locally or drop analytics temporarily. Losing some click counts is better than failing the user-visible hop.

Counters in Redis are fine as a fast derived view. The durable click log is the source of truth for later reconciliation.

18. Expiry, deletion, and custom aliases

Expiry

Store expires_at on the Link record. The redirect path rejects expired rows. A background job can delete or archive old rows so storage does not grow forever.

Deletion

Mark is_deleted or remove the row, then invalidate the cache. Cached 302 responses at a CDN should use a short max-age so deletion takes effect quickly.

Custom aliases

Treat them as user-chosen short codes with stricter validation:

length limits
allowed character set
reserved words: login, admin, api
unique constraint

They use the same table. No second data model is required.

19. Security and abuse

A public shortener is an abuse magnet.

Open redirects and malware

Validate URLs:

  • require http or https;
  • reject javascript URLs and malformed hosts;
  • optionally scan destinations asynchronously and disable confirmed-malicious links.

The first click may still go through. A warning interstitial is a product choice, not required for the core design.

Enumeration

Random 7-character codes make scanning expensive. Still:

  • do not list all codes;
  • rate-limit unknown-code lookups per IP;
  • cache negative lookups;
  • keep unpublished links unguessable.

Sequential counters without mixing are weaker here.

Rate limits and spam

Limit creates per user and IP. Limit redirects per IP only enough to stop obvious abuse; a viral link must still work for real users.

Data protection

Do not log full destination URLs with personal query parameters if we can avoid it. Encrypt in transit. Restrict who can fetch another user’s link list.

20. Failure scenarios

Creates fail. Redirects can continue from cache for hot keys. Cold keys return errors. This is why a high cache hit rate matters.

Redis unavailable

Redirects fall back to the store. Protect the database with admission control and serve only as much missed traffic as it can take.

Kafka unavailable

Redirects still succeed. Analytics lag or drop. Alert on unpublished click-buffer size.

Viral spike

  • Edge cache absorbs repeated lookups.
  • Local LRU protects Redis from identical keys.
  • Analytics consumers scale on lag.
  • Create path is unaffected if we isolated it from redirect pools.

Wrong redirect after delete

Use short cache TTLs and explicit invalidation. Prefer 302 so browsers do not keep a permanent mapping.

21. Observability

The important user metric is redirect p99 latency, plus error rate for known codes.

Also track:

  • create QPS, uniqueness retries, and idempotency hits;
  • cache hit ratio and hot-key traffic;
  • negative-lookup rate (scanning);
  • Kafka click lag;
  • expiry/deletion failures;
  • store latency on cache misses.

Alert when redirect p99 rises, cache hit ratio collapses, or a single code dominates one cache shard.

22. Multi-region

Redirects should be answered near the user.

User
Geo DNS / anycast
Regional redirect service + cache
replicated Link Store

Creates can go to a primary region and replicate mappings to others. A newly created code must work immediately for the creator; replicate synchronously to the creator’s region or write to a globally consistent key for that row.

If a region fails, serve redirects from replicas. Reject creates rather than risk two regions minting the same custom alias unless we have a clear conflict rule.

23. Final architecture

CREATE
──────
Client
  → Gateway
  → Link Service
  → generate code / accept alias
  → insert unique mapping
  → warm cache
  → return short URL

REDIRECT
────────
Client
  → Gateway / edge cache
  → Redirect Service
  → cache lookup
  → store lookup on miss
  → 302 Location
  → emit click event

The reasoning chain is:

Redirects dominate creates
Make lookup a single key read
Cache popular codes
Keep uniqueness in the durable store
Move click counting off the hot path

24. Interview-ready summary

Key decisions to remember

  1. Redirects are the scale problem, not storage.
  2. Use a unique short_code as the lookup key.
  3. Prefer random base62 codes or range-allocated counters over truncated hashes.
  4. Enforce uniqueness in the database, not with a check-then-insert.
  5. Choose 302 if you need analytics or the ability to disable links.
  6. Cache short_code → long_url and invalidate on delete.
  7. Do not increment click counters on the redirect path.
  8. Handle hot keys; one viral link is a cache-shard problem.
  9. Rate-limit creates and unknown-code scans.
  10. Measure redirect p99 and cache hit ratio.

Likely interviewer follow-up questions

  • Hash, counter, or random IDs — which and why?
  • How do you avoid sequential enumeration?
  • 301 vs 302?
  • How do you handle custom alias conflicts?
  • What happens when Redis is down?
  • How do you count clicks without adding latency?
  • How long should short codes be?
  • How do you prevent shortening malware URLs?
  • How would you design this for multiple regions?
  • What if one link gets 70% of traffic?

Senior-level points that differentiate the answer

  • Treat uniqueness as a storage constraint, not an application guess.
  • Separate the redirect SLO from analytics freshness.
  • Explain why hashing looks elegant and still causes collisions and poor aliases.
  • Call out hot-key handling for viral links.
  • Use negative caching to survive scanners.
  • Put click events on a queue so a viral post cannot melt the Link Store.
  • Distinguish browser caching (301) from your own cache.

A 1–2 minute verbal answer

I would scope this to creating a short code and redirecting it to a long URL, with optional custom aliases, expiry, and basic click counts. At 100 million new links a month and a 100:1 read ratio, creates are about 40 per second and redirects about 4,000, peaking near 40,000. Storage is modest; the hot path is a key lookup.

I would store short_code → long_url as the source of truth with a unique constraint. Auto-generated codes would be random 7-character base62 strings, which are unique enough at this scale and harder to enumerate than a raw counter. Custom aliases use the same insert. An idempotency key prevents duplicate creates on retry.

Redirects read a cache first, then the store, and return 302 so we can disable links and record clicks. Click events go to Kafka; workers update counts asynchronously. If Redis is down, we read the store with admission control. If Kafka is down, we still redirect. The numbers I would watch are redirect p99 and cache hit ratio, plus hot keys for viral links.

For the broader interview framework around this problem, see the System Design Interview Complete Guide.