Building a Scalable URL Shortener
Source: Dev.to
What it does
- Convert
https://website.com/very/long/urlintoshort.com/aB3x - Redirect users from the short URL back to the original
- Handle 1,000+ writes/sec and 10,000+ reads/sec
- Never generate duplicate short codes
What makes it hard
- IDs must be globally unique – no collisions
- Read‑heavy workload (≈10:1 read/write ratio)
- Must scale horizontally
Napkin math
| Metric | Calculation | Approx. |
|---|---|---|
| Writes/sec | 100 million URLs/day ÷ (24 h × 3600 s) | ~1,160 writes/sec |
| Reads/sec | 1,160 writes × 10 | ~11,600 reads/sec |
| Total URLs over 10 years | 100 M × 365 days × 10 years | ~365 billion URLs |
Architecture – Hexagonal (Ports & Adapters)
I chose Hexagonal Architecture because it keeps business logic independent from infrastructure, making it easy to swap databases, frameworks, or transport layers without touching the core.
Key properties
- Strong consistency – no duplicate short codes, even during network partitions
- ACID transactions – ID generation and insertion happen atomically
- Simple operations – ~99 % of queries are direct key lookups
Database schema (PostgreSQL)
-- Sequence for generating unique IDs
CREATE SEQUENCE urls_id_seq;
-- Main table
CREATE TABLE urls (
id BIGINT PRIMARY KEY DEFAULT nextval('urls_id_seq'),
short_code VARCHAR(10) NOT NULL UNIQUE,
long_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Index for fast short_code lookups
CREATE UNIQUE INDEX idx_urls_short_code ON urls (short_code);
Why it works
idis the source of truth (sequence‑generated)short_codehas a unique index for O(log n) lookups- No index on
long_url(rarely queried, expensive)
Short code generation (Base62)
public class ShortCodeGenerator implements ShortCodeGeneratorPort {
private static final String BASE62 =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@Override
public String generate(long id) {
if (id 0) {
int remainder = (int) (n % 62);
sb.append(BASE62.charAt(remainder));
n /= 62;
}
return sb.reverse().toString();
}
}
Collision‑free guarantee
- PostgreSQL sequence guarantees unique IDs (
1, 2, 3, …) - Base62 encoding is bijective: different IDs produce different codes
- No retry logic needed
Capacity table
| Length | Possible URLs | Years at 1 k writes/sec |
|---|---|---|
| 5 chars | 916 million | 29 years |
| 6 chars | 56.8 billion | 1,800 years |
| 7 chars | 3.52 trillion | 111,000 years |
With 7 characters we comfortably exceed the projected 365 billion URLs.
Caching layer (Redis)
Typical request flow without cache:
- App → PostgreSQL:
SELECT * FROM urls WHERE short_code = 'aB3x' - PostgreSQL reads from index + table (disk I/O)
- PostgreSQL → App: returns result
- App → Client:
302redirect
Latency: 5–10 ms per request.
Cache‑first pattern
- Try Redis first.
- On miss, query the database.
- Store the result in Redis for subsequent reads.
Benefits:
- Resilience (system works even if Redis is down)
- Speed (most reads bypass the DB)
API
Create short URL
POST /api/v1/shorten
Content-Type: application/json
{
"url": "https://website.com/very/long/url"
}
Response
{
"shortCode": "aB3x"
}
Resolve short URL
GET /aB3x
GET /api/v1/aB3x
Response – 302 Found with Location: https://website.com/very/long/url
Why use 302 instead of 301?
301(permanent) is cached by browsers, preventing click‑through analytics.302(temporary) forces a request to the server each time, allowing click tracking and other metrics.
Project structure
src/main/java/org/lomeu/
├── application/
│ ├── service/
│ │ └── UrlService.java # use‑case orchestration
│ └── port/
│ ├── in/
│ │ └── UrlUseCase.java # inbound port
│ └── out/
│ ├── UrlRepository.java # outbound port
│ └── ShortCodeGeneratorPort.java
│
├── domain/
│ └── Url.java # pure domain model
│
├── infrastructure/
│ ├── http/
│ │ └── UrlController.java # HTTP adapter
│ ├── database/
│ │ ├── Database.java # connection pool
│ │ └── JdbcUrlRepository.java # persistence adapter
│ └── generator/
│ └── ShortCodeGenerator.java # Base62 implementation
│
├── config/
│ └── AppConfig.java
│
└── Main.java # entry point + shutdown hook
The full implementation is available at github.com/lucaslomeu/url-shortener.