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 Hashapproach-2-unified-incrementapproach-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:
POST /api/shorten
Content-Type: application/json
{
"url":"https://example.com/some/very/long/url"
}
Example response:
{
"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
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
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
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
