/ Back to notes

July 29, 2026

11 min read

backend / software-architecture / system-design

Why URL Shorteners Are the Hello World of System Design

A URL shortener looks tiny until identifiers, cache invalidation, analytics, abuse prevention, and global traffic turn it into a compact tour of real system design trade-offs.

Why URL Shorteners Are the Hello World of System Design

A URL shortener looks trivial until you try to design it honestly.

On the happy path, the system only needs to do two things: accept a long URL, generate a short code, and later redirect that short code to the original destination. That sounds small enough to fit in a coding exercise.

But the moment you add billions of links, low-latency reads, revocation, analytics, phishing prevention, and multi-region traffic, the problem stops being about string manipulation and starts becoming a compact tour of system design itself.

That is why I think URL shorteners are the hello world of system design. The product surface is tiny, but the decisions underneath it are real: identifiers, caching, consistency, abuse control, operational isolation, and the path from a simple regional service to a global one.

The first mistake is drawing the final architecture too early

System design discussions often go wrong in the first five minutes.

Instead of starting with requirements, people jump straight to a diagram full of Redis, Kafka, Kubernetes, Cassandra, and a CDN. That usually produces a familiar-looking architecture, but not necessarily a correct one.

For a URL shortener, the important questions come first:

  • Is the service public or internal?
  • Are links editable?
  • Can users choose custom aliases?
  • Do links expire?
  • Do disabled links need to stop working immediately?
  • How important are analytics, and do they need exact counts?
  • What level of unavailability is acceptable?
  • Is the service regional or global?

Those answers change the system more than the technology names do.

This is the real lesson behind the classic references on the topic. System Design Interview popularized the URL shortener as a teaching problem, but the part that matters is not memorizing one canonical diagram. It is understanding why each component appears and at what scale it starts paying for its own complexity.

Capacity planning is what gives the architecture shape

For a concrete exercise, assume this workload:

  • 100 million links created per month
  • 100 redirects for every created link
  • five years of retention
  • 10x traffic spikes over the average

That gives us roughly:

100,000,000 / 30 days / 86,400 seconds ~= 39 writes/second
39 * 100 ~= 3,900 redirects/second on average
3,900 * 10 ~= 39,000 redirects/second at peak
100 million * 12 months * 5 years = 6 billion links

At around 500 bytes per primary record, the mapping data alone lands near 3 TB before indexes, replicas, backups, and analytics.

Those numbers are not there to predict the future precisely. They exist to reveal the order of magnitude. Once you know you are dealing with billions of mappings and read-heavy traffic, some design choices become obvious:

  • the redirect path must stay simple
  • the mapping lookup should be optimized for key-based reads
  • analytics should not sit in the critical path
  • cache invalidation becomes a product concern, not just a performance concern

The hardest design choice is usually the identifier strategy

People often frame URL shorteners as a storage problem, but I think the more interesting center of the design is the short code itself.

Base62 is only an encoding. It is not a generation strategy.

That distinction matters because several very different approaches can produce a Base62-looking code:

  • hash the URL and truncate it
  • generate a numeric sequence and encode it in Base62
  • use a distributed ID scheme such as Snowflake and encode the result
  • generate a random Base62 code and rely on a unique constraint plus retry

Each option has a different trade-off profile.

Hashing looks elegant because it is deterministic, but truncation introduces collisions and the same URL may need multiple valid links with different owners, campaigns, or expiration dates. Sequence-plus-Base62 is extremely practical for a first implementation, but the codes become predictable and the sequence becomes a coordination point. Snowflake-style IDs help when multiple nodes must generate ordered identifiers without a central counter, but they add operational constraints around clocks and worker identities.

For a public URL shortener, I would start with eight random Base62 characters, a unique constraint, and retry on collision.

That gives me:

  • a massive keyspace
  • decentralized code generation
  • simple implementation
  • less obvious enumeration than sequential IDs

And it keeps the storage model straightforward:

short_code -> destination

The persistence layer does not need to be exotic on day one. In many real systems, PostgreSQL plus a cache is a better first choice than jumping immediately to a distributed NoSQL store.

The first architecture that actually makes sense

The initial design should match the operational reality of the service, not the fantasy version of global scale.

Initial URL shortener architecture with a load balancer, application API, Redis cache, PostgreSQL, and analytics queue.

A reasonable first version keeps the redirect path short and pushes analytics out of band.

In this version, the application is still one deployable unit, but it already separates responsibilities internally:

  • link management handles creation, validation, quotas, and moderation rules
  • redirect handling focuses on lookup, status checks, expiration, and fast HTTP responses

That matters because reads and writes have different shapes.

On the write side, the system needs to:

  1. validate the destination URL
  2. enforce product rules such as alias uniqueness or expiration limits
  3. generate the short code
  4. persist the record
  5. warm or invalidate cache entries

On the read side, the system should do much less:

  1. look up the short code
  2. verify status and expiration
  3. return a redirect
  4. publish an analytics event asynchronously

This is where Redis earns its place. A cache-aside pattern is a strong fit for a workload dominated by reads:

  • cache hit: return the destination immediately
  • cache miss: read from PostgreSQL, then populate cache

I would also add short-lived negative caching for unknown codes. Without it, bots can force repeated misses for random strings and turn the database into the first responder for traffic that should have been cheap to reject.

The important part is not the boxes, it is the behavior around them

A diagram like the one above is useful, but it is still the easy part.

The real design work shows up in behavioral questions.

Why 302 is usually the safer default

For a general-purpose shortener, 302 is often the right starting status code.

Permanent redirects such as 301 or 308 push more caching responsibility to clients, browsers, and intermediaries. That can reduce traffic to the service, but it also reduces control. If a destination changes, if a link is revoked, or if you need to stop a phishing campaign quickly, permanent caching works against you.

If links are editable, revocable, or subject to moderation, temporary redirects are the safer default.

Cache cannot become the source of truth

Fast cache lookups are useful only if revocation still works.

Imagine a malicious link disabled in the database while an old Redis entry continues serving traffic for another hour. The persistence layer would be correct, but the user-visible behavior would still be wrong.

That is why cache invalidation here is not just a technical nuisance. It is product behavior.

A sane first strategy is:

  • invalidate cache on update or disable
  • use bounded TTLs
  • keep disabled or removed codes represented explicitly
  • prioritize invalidation paths for moderation events

Analytics should not slow down the redirect path

An easy mistake is updating counters synchronously on every redirect. That turns a read-heavy workload into constant write amplification, especially for hot links.

It is usually better to return the redirect first and publish analytics events afterward. Those events can feed daily aggregates, dashboards, and warehousing pipelines without holding the user-facing request open.

That design also lets you be honest about consistency. The mapping from short code to destination wants near read-after-write behavior. Analytics often does not.

Security is part of the architecture, not a side note

A public shortener hides the destination domain, which is exactly why it is useful to both legitimate users and attackers.

So the first real architecture also needs abuse controls:

  • allow only http and https
  • reject dangerous schemes
  • rate limit by IP, user, and organization
  • reserve sensitive aliases
  • maintain moderation workflows
  • support fast takedowns
  • record changes for auditability

If the system ever fetches remote metadata to preview destinations, then SSRF becomes part of the design too. The architecture changes the moment the backend starts making outbound requests on behalf of user input.

What changes when the service becomes global

Once traffic becomes geographically broad, the first architecture starts to show its limits.

The redirect path wants regional proximity. Cache misses should not cross continents if that can be avoided. Viral links create hot keys. Regional failures become part of the availability story. And the management path starts to diverge even more clearly from the redirect path.

Global URL shortener architecture with global DNS, edge routing, regional redirect services, distributed storage, and event processing.

At global scale, latency and failure isolation push the redirect path closer to the edge while storage and analytics become more distributed.

A global design usually introduces a few structural changes:

  • edge routing through DNS, Anycast, CDN, or WAF layers
  • regional redirect services close to readers
  • distributed key-value storage for the primary mapping
  • region-local caching to absorb hot traffic
  • streaming pipelines for analytics instead of ad hoc counters

At that point, systems such as DynamoDB, Cassandra, or Bigtable become easier to justify because the primary access pattern is still simple:

short_code -> destination

But even then, a distributed database does not automatically solve the interesting problems.

Hot keys still exist. A single viral link can overwhelm one logical key regardless of how evenly the rest of the dataset is partitioned. Edge caching and regional caches matter more for that case than clever partition math alone.

Global design also forces a consistency decision you can avoid in a smaller system: when do you acknowledge link creation? After one region commits it, or only after the mapping is replicated more broadly? Lower write latency and wider read-after-write guarantees pull in opposite directions.

That is exactly the kind of trade-off that makes this problem worth studying.

What I would actually build first

If I had to build a real first production version today, I would keep the architecture intentionally modest:

  • stateless modular application
  • PostgreSQL as the source of truth
  • Redis with cache-aside reads
  • random eight-character Base62 codes
  • unique constraint plus retry on collision
  • 302 redirects by default
  • asynchronous analytics events
  • rate limiting, validation, and moderation hooks from day one

I would separate the codebase into two main internal modules:

  • link management
  • redirect

That gives me a clean path to split deployment boundaries later if the read path grows much faster than the administrative path.

Only after observing real bottlenecks would I consider:

  • physically separating the services
  • adding regional replicas
  • partitioning storage
  • moving the mapping to a distributed KV system
  • pushing more redirect logic to the edge
  • expanding the analytics pipeline into a larger streaming system

This is the balance I like in system design: solve today's problem with the minimum necessary complexity, but leave tomorrow's scaling path visible.

Why this problem is still worth studying

The URL shortener is not interesting because Base62 is clever. It is interesting because a tiny feature forces you to make real decisions about scale, correctness, risk, and evolution.

It teaches a few durable lessons:

  • requirements matter more than fashionable components
  • scale estimates are design tools, not trivia
  • caching improves latency but creates revocation problems
  • analytics belongs off the critical path
  • relational databases are often a rational first step
  • distributed storage is only useful when its operational cost matches the actual need
  • global traffic changes failure modes as much as it changes latency
  • abuse prevention is architecture, not decoration

That is why I keep coming back to this example.

A URL shortener is small enough to explain clearly, but deep enough to expose the habits that separate architecture theater from actual engineering judgment.

References

  • Alex Xu, System Design Interview - An Insider's Guide, Volume 1
  • Martin Kleppmann, Designing Data-Intensive Applications
  • RFC 4648: The Base16, Base32, and Base64 Data Encodings
  • RFC 3986: Uniform Resource Identifier (URI): Generic Syntax
  • Redis documentation on cache-aside caching
  • Twitter engineering notes on Snowflake-style ID generation
  • AWS DynamoDB guidance on partition key design
  • Google SRE guidance on latency, errors, traffic, and saturation
  • Michael T. Nygard, Release It!