Loading...

Design a URL Shortener

A URL shortener looks simple: you give it a long link, it gives you a short one. But it’s a great system design question because it forces you to think about APIs, data modeling, caching, scale, abuse, and reliability—all in one small product.

Imagine you’re building this for a company like DevsCall. You want short links for course pages, certificates, marketing campaigns, and tracking. At small scale, you can build it in a weekend. At large scale, you’ll run into problems fast: hot keys, cache pressure, write spikes, abuse traffic, and analytics load. This lesson walks through a production-friendly design without overcomplicating it.

Start with requirements that shape the architecture

The first step is deciding what “good” means for this system. A beginner mistake is jumping straight to Base62 keys and Redis without knowing the expectations.

For a typical interview-friendly URL shortener, you can assume:

You need to create a short link from a long URL. You need to redirect from short link to the original quickly. You want basic analytics like click counts and maybe referrer/device later. You want custom aliases optionally. You want expiration for links optionally. You must prevent abuse like bot traffic and brute-force scanning.

The main non-functional requirement is latency: redirects should feel instant, so reads must be extremely fast. The second is availability: redirects must work even if analytics is down. And the third is correctness: a short code must map to the right URL consistently.

Define APIs that match real product behavior

A clean API surface keeps your system easy to evolve.

A typical create endpoint looks like:

POST /v1/shorten with body containing the long URL, optional custom alias, optional expiry, and optional campaign metadata. The response returns the short URL and the code.

A redirect endpoint looks like:

GET /{code} which performs a 301 or 302 redirect. In many products, 302 is used initially (flexibility for change), while 301 is used for permanent links once stable.

Analytics endpoints are often separate, for example:

GET /v1/links/{code}/stats for aggregated metrics, and perhaps an admin endpoint for listing a user’s created links.

The key idea is keeping redirect path minimal: the redirect API should not wait on analytics writes.

Design the database schema for the core mapping first

The heart of the system is a simple mapping: code → long_url.

A practical relational schema might be:

links

  • code (primary key)
  • long_url
  • created_at
  • expires_at (nullable)
  • user_id (nullable if anonymous allowed)
  • is_active (soft delete / disable)
  • custom_alias (boolean or just inferred if code was user-provided)
  • metadata (optional JSON: campaign, tags)

You index by code because redirects are the main query. You may also index by user_id if you support dashboards.

If you expect very high scale, a key-value store works well too, because the lookup is a direct key read. Many real systems store the mapping in a KV store and keep relational for management/reporting.

Key generation is where most designs diverge

You need short, unique, non-guessable-ish codes. The choice affects scalability and abuse risk.

A common choice is Base62 encoding (a–z, A–Z, 0–9) because it generates compact strings. The question becomes: what number are you encoding?

One reliable approach is using a database-generated numeric ID, then encoding it to Base62. It’s simple and fast, but it creates predictable codes, which makes scanning easier. Predictability isn’t always a security issue, but it increases abuse potential.

If you want less predictability, you can add a secret salt and use a reversible obfuscation step, or generate random codes.

Random codes reduce predictability but require collision handling. You generate a code, check if it exists, retry if it collides. With enough length (like 7–10 chars Base62), collisions become rare, but you still design retries.

Custom aliases are handled as a special case: if a user requests devscall-ai, you must check availability and reserve it transactionally.

In interviews, the strongest answer is: start with ID→Base62 for simplicity, add anti-enumeration protections (rate limiting, bot detection), and move to random/obfuscated codes if abuse becomes a real problem.

Redirect path should be read-optimized with caching

The redirect path is the hottest path. The goal is to do as little work as possible.

A standard production pattern is:

  1. Check cache (Redis / in-memory) for code -> long_url
  2. If cache hit, redirect immediately
  3. If cache miss, read from DB/KV store
  4. Populate cache with TTL and redirect

Caching here is extremely effective because popular links are accessed repeatedly. You typically cache only active, non-expired links, and choose a TTL that balances freshness and memory usage.

To handle expired or disabled links, you can store a negative cache entry for “not found” briefly to reduce repeated DB hits from bots.

Rate limiting matters more than people expect

URL shorteners attract abuse: scanners, bots, brute forcing valid codes, and traffic amplification.

You want rate limiting in two places:

Creation rate limiting stops someone from generating millions of links. This is usually per user/IP with stricter limits for anonymous users.

Redirect rate limiting is trickier because real links can go viral. You usually don’t want to block real traffic. Instead, you use bot rules, WAF/CDN protections, anomaly detection, and possibly per-IP soft limits for suspicious patterns (like sequential code scanning).

In a system design interview, calling this out is a strong signal: the biggest threat to URL shorteners is not scaling, it’s abuse.

Analytics should be async so redirects stay fast

If you try to write a click record synchronously on every redirect, you will slow down redirects and overload your primary store.

Instead, treat analytics as a separate pipeline:

On redirect, emit an event like “link_clicked” containing code, timestamp, ip prefix, user agent, referrer, maybe geo. Push it into a queue (Kafka, SQS, RabbitMQ).

A consumer aggregates counts into a fast store such as a time-series DB, a column store, or even Redis counters + periodic flush to DB.

This design makes the redirect path stable even if analytics is down. Worst case, you lose some analytics events, but redirects still work—which is the right tradeoff.

Scaling the system: what actually grows first

At scale, the read path dominates. Redirects can be orders of magnitude higher than creates.

You scale reads with caching and replication. If your mapping store is relational, read replicas help, but cache typically handles the bulk. If mapping is in a distributed KV store, scale is even easier because reads are already partitioned.

For writes (link creation), volume is usually smaller, but you still design it cleanly. If you use DB autoincrement IDs, you ensure the DB can handle that write rate. If you use random code generation, you ensure collisions are handled efficiently.

CDN can also help. Many companies put a CDN in front of redirects, but redirects are dynamic responses, so CDN caching is limited. Still, CDNs are useful for blocking abuse traffic and handling TLS termination at the edge.

Failure modes you should explicitly design for

A strong design always answers: “What breaks, and what happens then?”

If cache is down, redirects still work by reading from DB/KV, but latency increases. You should be able to survive this temporarily.

If DB/KV is down, redirects fail. This is the highest severity. You mitigate with replication, multi-AZ, fast failover, and strong monitoring.

If analytics pipeline is down, redirects should still work. Events may queue up or be dropped depending on your design, but user experience stays healthy.

If key generation service fails, creation fails but redirects should still work. This is a good separation of concerns: serving existing links is more important than creating new ones during incidents.

Wrap-up: the simplest “good” architecture

A production-friendly URL shortener usually looks like this:

Application/API service handles create + redirect. Redis caches code lookups. A primary store holds the code mapping. A queue captures click events. Analytics consumers aggregate stats into a separate store. A CDN/WAF protects the edge.

It’s not complicated, but it is intentional: fast reads, safe writes, async analytics, and graceful failure behavior.

Frequently Asked Questions

It tests multiple core concepts at once, including APIs, databases, caching, scalability, abuse prevention, and reliability, in a simple-looking product.

Fast and reliable redirects. Reads are far more frequent than writes, so low latency and high availability matter most.

Common approaches include encoding database IDs using Base62 or generating random strings with collision checks. Each has tradeoffs.

They use rate limiting, bot detection, IP throttling, and edge protections to stop brute-force scanning and spam link creation.

Cache outages, database failures, key generation issues, and analytics pipeline downtime—and how the system degrades gracefully.

Still have questions?Contact our support team