Building a Scalable URL Shortener

Published: (February 10, 2026 at 06:08 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

What it does

  • Convert https://website.com/very/long/url into short.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

MetricCalculationApprox.
Writes/sec100 million URLs/day ÷ (24 h × 3600 s)~1,160 writes/sec
Reads/sec1,160 writes × 10~11,600 reads/sec
Total URLs over 10 years100 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

  • id is the source of truth (sequence‑generated)
  • short_code has 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

LengthPossible URLsYears at 1 k writes/sec
5 chars916 million29 years
6 chars56.8 billion1,800 years
7 chars3.52 trillion111,000 years

With 7 characters we comfortably exceed the projected 365 billion URLs.

Caching layer (Redis)

Typical request flow without cache:

  1. App → PostgreSQL: SELECT * FROM urls WHERE short_code = 'aB3x'
  2. PostgreSQL reads from index + table (disk I/O)
  3. PostgreSQL → App: returns result
  4. App → Client: 302 redirect

Latency: 5–10 ms per request.

Cache‑first pattern

  1. Try Redis first.
  2. On miss, query the database.
  3. 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

Response302 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.

0 views
Back to Blog

Related posts

Read more »