Brainy Blueprints · Part 1

How We'd Design a URL Shortener

BrainyTech Team
August 14, 2026
6 min read

A weekly series where we break down real systems — from the business problem, to the architecture, to the exact data structure that makes it work.

The Problem

Services like bit.ly or TinyURL take a long URL and return a short one that redirects back to the original. Simple to describe, but designing it properly at scale surfaces real engineering decisions — which is exactly why it's one of the most commonly discussed system design problems.

Let's design it the way we'd actually approach it for a client.

Step 1: Define the Requirements

Functional requirements:

  • Given a long URL, generate a unique short URL
  • Given a short URL, redirect to the original long URL
  • Links should not collide — two different long URLs should never map to the same short code
  • (Optional) Support custom aliases and link expiration

Non-functional requirements:

  • High availability — a link redirect service going down is highly visible and damaging
  • Low latency — redirects need to feel instant
  • Scale — a real-world service needs to handle billions of URLs and heavy read traffic (redirects vastly outnumber creations)

Notice we haven't written a line of code yet. This is the part that separates a considered system from a guess — and it's the part we spend real time on with clients before touching implementation.

Step 2: High-Level Design

At a high level, we need:

  1. A write path — takes a long URL, generates a short code, stores the mapping
  2. A read path — takes a short code, looks up the long URL, issues a redirect
  3. A datastore — a simple key-value store works well here (short code → long URL), since lookups are always by exact key

Because reads dominate writes by a huge margin in a real link shortener, a caching layer (like Redis) in front of the database is a natural addition — most popular links get looked up repeatedly, and serving those from memory instead of hitting the database every time is a meaningful performance win.

Step 3: The Data Structure Underneath — Hashing & Base62 Encoding

This is the core algorithmic decision, and it's worth being precise about what we're actually doing.

We are not compressing the URL. Compression is reversible and complex; what we actually want is a short, unique identifier. The clean way to get that:

  1. Every new URL gets a unique, auto-incrementing numeric ID (e.g., from the database, or a distributed ID generator at scale)
  2. That numeric ID is encoded into Base62 — using the 62 characters A-Z, a-z, 0-9 — which turns a large number into a short string

Why Base62 specifically? Because it's URL-safe (no special characters to escape) and dense — a 7-character Base62 string can represent numbers up to 62^7, which is over 3.5 trillion unique IDs. That's more than enough headroom for a real product.

Python Implementation

python
BASE62_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

def encode_base62(num: int) -> str:
    """Convert a unique numeric ID into a short Base62 string."""
    if num == 0:
        return BASE62_CHARS[0]
    result = []
    base = len(BASE62_CHARS)
    while num > 0:
        num, remainder = divmod(num, base)
        result.append(BASE62_CHARS[remainder])
    return ''.join(reversed(result))

def decode_base62(short_code: str) -> int:
    """Convert a Base62 short code back into its original numeric ID."""
    base = len(BASE62_CHARS)
    num = 0
    for char in short_code:
        num = num * base + BASE62_CHARS.index(char)
    return num


# Example usage
url_id = 125_000_000
short_code = encode_base62(url_id)
print(short_code)             # e.g. "8M0kQ"
print(decode_base62(short_code))  # 125000000

Because every ID is unique by construction (it comes from an auto-incrementing counter or distributed ID generator), collisions are structurally impossible — we're not hashing the URL content and hoping for no clashes, we're encoding a guaranteed-unique number. This is a subtle but important distinction from naive approaches that hash the URL string itself and then have to handle collision retries.

Step 4: Trade-offs at Scale

This is the part that separates a whiteboard answer from something we'd actually ship for a client.

The bottleneck: A single auto-incrementing counter becomes a single point of contention once you have multiple API servers trying to generate IDs simultaneously.

The fix: At real scale, you'd replace the simple auto-increment with a distributed unique ID generator — something like Twitter's Snowflake algorithm, which generates unique IDs made up of a timestamp, a machine ID, and a sequence number, so multiple servers can generate IDs independently without coordinating on every single request.

Other real considerations we'd flag to a client:

  • Custom aliases need a separate uniqueness check against the datastore before insert
  • Analytics (click counts per link) benefit from an async write path — don't block the redirect on writing analytics data
  • Link expiration needs a cleanup strategy — a background job (or TTL if using something like Redis) rather than checking on every read

Why We Build This Way

The algorithm — Base62 encoding — is genuinely the easy part. The value we bring on a project like this is in the requirements gathering, the read/write path design, the caching strategy, and knowing which trade-offs actually matter at your specific scale versus which ones are premature optimization.

That's the thinking we try to make visible in this series — not just "here's some code," but here's how we'd actually reason through it with you.


This is Week 1 of Brainy Blueprints, our weekly series breaking down real systems from business problem to production code. Next week: the LRU Cache, and how it powers API response caching.

📩 Want to talk through a system design problem for your product? [email protected]

Share this article:
← Back to Blog