Skip to main content
← Back to Case Studies
Case Study

Designing a Scalable URL Shortener: One Problem, Three ID Generation Strategies

shorty a simple url shortener designed for scale and performance

•5 min read•362 views
BackendREST APIweb developmenttechnical excellenceDatabase
Designing a Scalable URL Shortener: One Problem, Three ID Generation Strategies
◎

The Problem

URL shorteners are one of those projects every backend engineer builds at some point. On the surface they’re simple: store a URL, generate a short code, redirect users.The interesting part isn’t the redirect , it’s deciding how those short codes are generated. To explore the trade-offs , I built the same URL shortener three different times, changing only one thing: the ID generation strategy

Source Code

Repository: https://github.com/MohamedBenTaher/shorty

Branches:

  • main
    — Random Hash
  • approach-2-unified-increment
  • approach-3-distributed-increment

The Problem

A production-ready URL shortener needs uniqueness, idempotency, fast redirects, and no single point of failure.
The rest of the article compares three ways of generating IDs while keeping the API, schema, and redirect logic identical.

Request Flow

Client
  │
POST /shorten
  │
Lookup existing URL
  │
Generate ID
  │
Base62 encode
  │
Insert into PostgreSQL
  │
Return short URL

Example request:

http
POST /api/shorten
Content-Type: application/json

{
  "url":"https://example.com/some/very/long/url"
}

Example response:

json
{
  "shortCode":"bX93Pa",
  "shortUrl":"http://localhost:8080/bX93Pa"
}
✓

The Solution

Base62

Decimal Base62
1 1
10 A
35 Z
36 a
61 z
62 10
3843 zz

Approach 1 — Random Hash

mermaid
flowchart LR
Client-->API-->Check["Check existing URL"]
Check-->|Exists|Return
Check-->|New|Random["Generate Secure Random 64-bit"]
Random-->Base62-->DB[(PostgreSQL)]-->Return

Generate a cryptographically secure 64-bit integer, encode it with Base62, and store it. Collisions are extraordinarily unlikely and can simply be retried.

Pros

  • No coordination
  • Horizontally scalable
  • Hard to enumerate

Cons

  • Longer (~11 char) codes
  • No chronological ordering

Approach 2 — Unified Increment

mermaid
flowchart LR
Client-->API-->Seq["PostgreSQL Sequence"]-->Base62-->DB[(PostgreSQL)]-->Return

PostgreSQL owns the counter. IDs are sequential, compact, and trivial to encode.

Spring Data JDBC requires marking the entity as new because IDs are assigned before insertion.

Pros

  • Very short codes
  • Extremely simple
  • Guaranteed uniqueness

Cons

  • Central sequence
  • Predictable IDs

Approach 3 — Distributed Snowflake

mermaid
flowchart LR
A["Timestamp (42 bits)<br/>Milliseconds since Jan 2025"]
-->B["Shard ID (10 bits)<br/>0–1023"]
-->C["Sequence (12 bits)<br/>0–4095 per ms"]

Snowflake embeds time, machine identity, and a per-millisecond sequence into one 64-bit integer.

Pros

  • No coordination
  • Multi-region ready
  • Time-ordered IDs

Cons

  • More implementation complexity
  • Longer codes

Complexity

Operation Complexity
Create short URL O(1) + indexed writes
Redirect lookup O(log n) via B-tree index
Duplicate lookup O(log n)

Comparison

Feature Random Sequence Snowflake
Coordination None DB sequence None
Multi-region Yes No Yes
Enumerable No Yes No
Typical length ~11 1–7 ~11
Complexity Low Low Medium

Final Thoughts

Before building these implementations, I expected Snowflake to be the obvious winner.

Instead, the biggest takeaway was that each strategy shines under different constraints. A PostgreSQL sequence is often the simplest and best choice for internal tools. Random IDs are ideal when unpredictability matters. Snowflake earns its complexity once a system becomes genuinely distributed.

The best solution isn’t determined by the algorithm—it’s determined by the constraints you’re optimizing for.

Tech Stack

  • Java 21
  • Spring Boot 3.4
  • Spring Data JDBC
  • PostgreSQL
  • Docker Compose
  • HTML/CSS/JavaScript
  • JUnit 5
  • Mockito
  • Gradle
  • Base62 Encoding
  • Snowflake Algorithm