Skip to main content
PProfileMatch

EasyProblem 1 of 36 in the system design series

Design Bitly: A URL Shortener for Billions of Redirects

Prefer watching? This article is the written companion to the video above, from the Shubham Aher YouTube channel.

A URL shortener takes a long, unwieldy address and hands back a compact link such as bit.ly/3xK9pQ. When someone opens that compact link, the service looks up the original address and bounces the browser there. The whole product is two operations: make a link, follow a link. That small surface is exactly what makes it a favourite opening question in system design interviews.

Interviewers like it because a junior and a senior candidate can both produce a working answer, yet their answers look very different under pressure. The basic version is a single table and a web server. The strong version explains how codes stay unique without a database round trip per write, how a redirect finishes in a few milliseconds when one link goes viral, and what happens when a cache node or the ID source falls over.

The hard parts are therefore not the features but the properties around them: uniqueness, latency and availability for a workload where reads outnumber writes by orders of magnitude. The design below for a Bitly-like service is built around that imbalance, and every choice traces back to it.

This is an interview practice design for a Bitly-like service, built from first principles. It is not a description of how Bitly is actually built.

Functional Requirements

Scope the problem before drawing anything. The two core features are shortening and redirecting. A user submits a long URL and receives a short one; later, anybody holding the short link, logged in or not, should land on the original page. Everything else in the system exists to make those two flows correct and quick.

Two optional features are worth naming because they change the data model. A custom alias such as my-sale means codes are not always machine-generated, so the system must reject an alias that is already taken. An expiration date means a stored mapping can become invalid, which affects caching and cleanup later. Click analytics is deliberately parked: it is a separate pipeline and would distract from the core path during the first half of the interview.

Create a short URL

Give us a long URL, get back a short one.

long URL → short code

Redirect

Anyone opening the short link lands on the original page.

GET /code → long URL

Custom alias

Optional: choose your own code, like my-sale.

optional

Expiration

Optional: the link stops working after a date.

optional

  • Out of scope today: click analytics (covered at the end)

Non-Functional Requirements

The non-functional list is where the design gets its shape. Redirects should complete in under 100 ms, because a short link is just a detour on the way to the page the user actually wants, and any delay feels like the destination site is slow. Availability matters just as much: assume a 99.99% target, since a shortener outage breaks links embedded in emails, posters and social posts everywhere at once.

Uniqueness is a correctness requirement, not a nice-to-have. If two long URLs ever share a code, someone gets sent to the wrong page, which is both a bug and a potential security problem. Finally, the workload is lopsided: a link is created a single time and then clicked over and over. That imbalance tells us to spend most of our engineering effort on the lookup path rather than the creation path.

Low latency

Redirects must feel instant.

< 100 ms

High availability

If we are down, links break across the internet.

99.99% uptime

Unique codes

Two long URLs never share a short code.

no collisions

Read-heavy scale

Created once, clicked again and again.

reads ≫ writes

Scale Estimation

For the estimate, assume roughly 1 billion stored short URLs and around 100 million daily active users who together trigger about 1 billion redirects per day. Spread across 86,400 seconds, that comes to roughly 12K redirects per second on average. Traffic is never flat, so plan for a peak of around 50K per second when a popular link is shared widely.

Storage turns out to be modest. If each row holds the code, the long URL, an owner, timestamps and an optional expiry in about 500 bytes, a billion rows is roughly 500 GB. That fits comfortably on a handful of machines, so storage volume is not the bottleneck. The number that drives the architecture is the read-to-write ratio of about 1000 to 1: optimise the redirect path first, and keep the creation path simple and correct.

1billion

Short URLs stored

100million

Daily active users

≈ 1 billion redirects / day

12K avg

Redirects per second

≈ 50K at peak

500GB

Storage (500 B / row)

fits on a few machines

Read : write ≈ 1000 : 1 → design the read path first

Core Entities & API

The data model needs only three entities: the original URL, the short URL identified by its code, and the user who created it. Keeping the model this small makes the lookup a pure key-value access, which pays off later when we shard and cache.

Creation is a POST /urls call whose body carries the long_url plus optional custom_alias and expires_at fields, and the response returns the finished short URL. Following a link is a plain GET /{short_code} that answers with an HTTP redirect pointing at the long URL. Notice the redirect endpoint sits at the root path so the short link stays as short as possible, and it needs no authentication, since anyone holding the link should be able to use it.

Core entities

Original URLShort URL (code)User

API

  • POST /urls { long_url, custom_alias?, expires_at? }→ { short_url }
  • GET /{short_code}→ 302 redirect to long_url

High-Level Design

Requests from browsers and apps first reach a load balancer, which distributes them across a fleet of application servers. Because creation and redirection have such different traffic profiles, the design splits them into a write service and a read service. Each can then be scaled, deployed and tuned on its own terms; a burst of clicks on a viral link never slows down link creation.

On the write side, the service asks a counter for the next unique integer, encodes it into a short code, and stores the code-to-URL mapping in the database. On the read side, the service checks a Redis cache before touching the database, because a small fraction of links receives most of the clicks. A miss falls through to the database, and the result is written back into the cache for the next visitor.

Neither service keeps any session or per-user state in memory. That is the property that lets us add instances freely behind the load balancer as traffic climbs, and it means a crashed instance can be replaced without any data recovery step.

next IDsave mapping1 · check cache2 · on missClientbrowser · appLoad balancerspreads trafficWrite servicecreate short URLCounterRedis INCRDatabasecode → long URLRead serviceredirectsCacheRedis · hot links
High-level design. Dashed arrows are taken only on a cache miss or as a fallback. Scroll sideways on small screens.

Stateless services → add instances as traffic grows

Deep Dive 1 — Unique Short Codes

There are three common ways to mint a code. Hashing the long URL (for example MD5) and keeping the first seven characters is easy and even deduplicates identical URLs, but truncation creates collisions, so each write must check the database and retry with a tweak. Generating a random string has similar issues: it is unpredictable, which is nice, yet every write still needs an existence check, and the chance of a clash rises as the table fills.

The chosen approach is a global counter encoded in base62, the alphabet of digits plus lowercase and uppercase letters. Each new link receives the next integer, and since integers never repeat, the codes never repeat either, so no collision check is needed. Seven base62 characters cover 62 to the power of 7, roughly 3.5 trillion values, which is thousands of times the billion links we assumed.

A single counter that every write hits would become a hot spot, so each write server reserves a block of, say, 1,000 IDs from Redis and hands them out locally. The shared counter is touched only once per thousand links. If predictable sequential codes worry you, apply a reversible scrambling function to the integer before encoding; uniqueness is preserved and neighbouring links no longer look alike.

Hash + truncate

  • + Simple
  • + Same URL → same code
  • − Collisions → check & retry

Random code

  • + Unpredictable codes
  • − Collisions grow with size
  • − Extra DB lookup per write

Counter + base62Chosen

  • + Always unique
  • + No collision checks
  • − Needs a fast counter
  • − Sequential → guessable
  • 7 base62 chars → 62⁷ ≈ 3.5 trillion codes
  • Each server grabs a batch of 1,000 IDs from Redis
  • Worried about guessable codes? Shuffle the ID with a reversible function first

Deep Dive 2 — Fast Redirects

Meeting the 100 ms target is about stopping each request as early as possible. The outermost layer is a CDN or edge cache that can answer redirects for the hottest links from a location near the user, so those requests never cross the ocean to our servers. Anything the edge cannot serve reaches the read service, which looks in Redis. A Redis lookup takes well under a millisecond, and with a heavily skewed click distribution most requests end there.

Only on a cache miss does the read service query the database, and even that is cheap because the short code is the primary key. The fetched mapping is written back into Redis with LRU eviction, which naturally keeps popular links resident and lets stale ones age out.

The final step is choosing the status code. A 301 tells the browser the move is permanent, so it remembers the destination and skips us on later clicks, which saves load. A 302 marks the move as temporary, so every click comes back through our servers. Pick 302 if you need click counts, expiring links or the ability to edit a destination; pick 301 if raw traffic reduction matters more.

  1. 1CDN / edgeHot links answered close to the user
  2. 2Redis cacheSub-millisecond lookup; most requests stop here
  3. 3DatabaseOnly on a miss — primary-key lookup by code
  4. 4Fill the cacheLRU eviction keeps popular links hot
  5. 5Redirect301 or 302?
  • 301 permanent → browser caches it → less load on us
  • 302 temporary → every click reaches us → analytics, expiry, editable links

Deep Dive 3 — Scaling & Availability

Scaling follows from the earlier choices. The read service is stateless, so handling more clicks means adding instances behind the load balancer. The database stores simple key-value pairs, which makes sharding by short code straightforward: hash the code to pick a shard, and attach read replicas when lookups outgrow a single primary. The cache becomes a Redis cluster, spreading hot keys across nodes and promoting a replica if a node dies.

The counter needs special care because it is the one component that must never repeat itself. Persist its value so a restart cannot hand out an already-used range. The batching from the code-generation deep dive also buys resilience: servers holding unused IDs can keep creating links through a brief counter outage. Finally, a scheduled background job deletes expired mappings so the table does not grow without bound and stale links stop resolving.

  • Scale the read service horizontally

    Stateless — just add servers behind the load balancer.

  • Shard the database by short code

    A simple key-value lookup; add read replicas for more read capacity.

  • Redis cluster for the cache

    Popular keys spread across nodes; replicas take over on failure.

  • Make the counter safe

    Persist it so a number is never handed out twice; batches survive short outages.

  • Clean up expired links

    A background job removes them so storage doesn't grow forever.

Trade-offs

Every decision here had a cost. Choosing between 301 and 302 trades lower traffic for control and visibility over each click. A counter guarantees unique codes, but you now operate a small piece of critical infrastructure that must be durable. Caching makes redirects fast, yet a deleted or expired link has to be evicted from the cache as well, or users will keep landing on a destination that should be gone.

The storage engine is less contentious than candidates often fear. A key-value store shards effortlessly for this access pattern, while a relational database offers constraints such as a unique index on custom aliases. Either is defensible if you explain the reasoning. The thread running through the whole design is simple: favour the read path, make uniqueness structural rather than checked, and keep services free of local state so they can scale out.

  • 301 vs 302

    Less traffic to us — or full control and analytics.

  • Counter vs hashing

    Guaranteed uniqueness — but we must run a reliable counter.

  • Caching

    Very fast reads — but deleted or expired links must be removed from the cache too.

  • SQL or NoSQL

    Both work: key-value scales easily; SQL gives constraints like a unique custom alias.

Optimise for reads. Guarantee uniqueness. Keep services stateless.

Key takeaways

  • Reads outnumber writes by about 1000 to 1, so the redirect path gets the most design attention.
  • A batched counter plus base62 encoding makes every short code unique without collision checks.
  • Seven base62 characters give roughly 3.5 trillion codes, far more than a billion links need.
  • Edge cache, then Redis, then a primary-key database lookup keeps redirects well under 100 ms.
  • Choose 302 for analytics and editable links, 301 to let browsers cache and reduce load.
  • Stateless services, a sharded key-value store and a durable counter keep the system available.

Interview questions

How does a URL shortener generate unique short codes?

A reliable approach is a monotonically increasing counter whose value is converted to base62 using digits and both letter cases. Since no integer is issued twice, no code is issued twice, and the write path skips collision checks entirely. To avoid a central bottleneck, each application server reserves a block of IDs, for example 1,000 at a time, and assigns them locally until the block runs out.

How many characters should a short URL code have?

Seven characters from a 62-symbol alphabet give 62 to the power of 7, about 3.5 trillion combinations. If you assume around one billion links, that leaves enormous headroom for growth. Six characters would give about 57 billion values, which also works for many workloads, but seven is a comfortable default that avoids running out for many years.

Should a URL shortener use a 301 or 302 redirect?

Use 301 when you want browsers to remember the destination and stop calling you, which cuts load. Use 302 when every click must reach your servers, for example to count clicks, enforce expiry dates, or let owners change where a link points. Many shorteners lean toward 302 because analytics and control usually matter more than the saved requests.

Why not just hash the long URL to make a short link?

Hashing works, but keeping only the first few characters of a hash means different URLs can map to the same prefix. Each write then needs a database lookup to detect clashes and a retry strategy when one occurs. As the table grows, those checks become more frequent. A counter-based scheme avoids the problem by construction, so writes stay simple and predictable.

How do you keep URL shortener redirects under 100 milliseconds?

Layer the lookups so most requests never reach the database. An edge cache or CDN answers the most popular links near the user, a Redis cache with LRU eviction handles most of the rest in under a millisecond, and the database is consulted only on a miss through a primary-key lookup. Stateless read servers let you add capacity as traffic rises.

Keep practising