Skip to content

Ryanditko/clj-url-shortener

Repository files navigation

Clojure URL Shortener

System Overview

Clojure Datomic Apache Kafka Pedestal Redis Docker

Status License Tests

A production-grade URL shortener written in Clojure, following the Diplomat Architecture pattern. Uses Datomic for immutable URL and user storage, Redis for high-performance caching, and Apache Kafka for real-time click event streaming and analytics aggregation. Includes JWT authentication with user registration, per-IP rate limiting, CORS support, Prometheus metrics, and a full CI/CD pipeline via GitHub Actions.


Stack

Icon Concern Technology
Clojure Language Clojure
Pedestal HTTP Server / API Gateway Pedestal + Ring
Datomic Database / URL & User Storage Datomic
Redis Caching Redis
Kafka Event Streaming Apache Kafka
Schema Schema Validation Plumatic Schema
Prometheus Observability Prometheus Metrics
Docker Containerization Docker + Docker Compose
Testing Testing clojure.test
CI CI/CD GitHub Actions

Architecture

Follows the Diplomat Architecture (Hexagonal Architecture variant), strictly separating domain logic from infrastructure. Each layer has a single responsibility and well-defined access rules.

graph LR
    subgraph external [External World]
        HTTPClient([HTTP Clients])
        KafkaBrokerExt([Kafka Broker])
    end

    subgraph boundary [Boundary Layer]
        Diplomats[Diplomats]
        Wire[Wire Schemas]
        Adapters[Adapters]
    end

    subgraph domain [Domain Core]
        Controllers[Controllers]
        LogicLayer[Logic]
        Models[Models]
    end

    external --> boundary
    boundary --> domain
    Diplomats --> Wire
    Diplomats --> Adapters
    Adapters --> Wire
    Adapters --> Models
    Controllers --> LogicLayer
    Controllers --> Models
    LogicLayer --> Models
Loading
  • Models - Pure domain entities (Url, UrlStats, ClickEvent) defined with strict Prismatic Schemas. No dependencies on any other layer.
  • Logic - Pure business rules without side effects: Base62 encoding, URL validation, expiration calculation, click counting, statistics aggregation, JWT token management, password hashing, and rate limiting.
  • Controllers - Use case orchestration following the logic sandwich pattern: consume data from diplomats, compute with pure logic, produce side effects through diplomats.
  • Adapters - Pure transformation functions between wire schemas and domain models. Inbound adapters convert loose external data into strict internal models; outbound adapters do the reverse.
  • Wire - External data contracts. wire.in uses loose schemas (tolerant reader), while wire.out, wire.cache and wire.datomic use strict schemas (conservative writer).
  • Diplomats - All external communication: HTTP server (Pedestal), database (Datomic), cache (Redis), event streaming (Kafka producer and consumer). Each diplomat is fault-tolerant and manages its own Component lifecycle.

See ARCHITECTURE.md for the full specification with layer access rules.


Data Flow

sequenceDiagram
    participant C as Client
    participant P as Pedestal
    participant RL as Rate Limiter
    participant Auth as JWT Auth
    participant Ctrl as Controller
    participant D as Datomic
    participant R as Redis
    participant K as Kafka

    C->>P: POST /api/auth/register
    P->>RL: Check rate limit
    P->>D: Save user (hashed password)
    P->>C: 201 Created

    C->>P: POST /api/auth/login
    P->>RL: Check rate limit
    P->>D: Find user, verify password
    P->>C: 200 {token, expires-in}

    C->>P: POST /api/urls (with Bearer token)
    P->>RL: Check rate limit
    P->>Ctrl: create-url!
    Ctrl->>D: save-url!
    Ctrl->>R: cache-url!
    Ctrl->>K: publish url.created
    P->>C: 201 Created

    C->>P: GET /r/:code
    P->>RL: Check rate limit
    P->>Ctrl: redirect-url!
    Ctrl->>R: get-cached-url
    R-->>Ctrl: cache hit / miss
    Ctrl->>D: find-url (on miss)
    P->>C: 302 Redirect
    Ctrl-->>K: publish url.accessed (async)
    K-->>D: Consumer aggregates analytics
Loading

API

Method Endpoint Auth Description
GET /health Public Health check
GET /metrics Public Prometheus metrics
POST /api/auth/register Public Register a new user
POST /api/auth/login Public Authenticate and get JWT token
POST /api/urls Public Shorten a URL
GET /r/:code Public Redirect to original URL
GET /api/urls/:code/stats Public Get click statistics
GET /api/urls/:code/analytics Required Get daily analytics breakdown
DELETE /api/urls/:code Required Deactivate a short URL

Security

  • User Registration - Users register via /api/auth/register with username and password (min 8 chars). Passwords are hashed with bcrypt+SHA512 and stored in Datomic.
  • JWT Authentication - Protected endpoints require a Bearer token obtained via /api/auth/login with valid credentials.
  • Rate Limiting - Per-IP token bucket: 30 req/min for API, 100 req/min for redirects, 5 req/min for auth endpoints (brute force protection).
  • CORS - Cross-origin requests are supported with configurable origin headers.
  • 429 Too Many Requests - Includes Retry-After header when rate limit is exceeded.

Observability

  • /metrics endpoint exposes Prometheus-compatible metrics.
  • http_requests_total - Request count by method, path, and status.
  • http_request_duration_seconds - Request latency histogram.
  • urlshortener_urls_created_total - Business metric for URL creation.
  • urlshortener_redirects_total - Business metric for redirects.
  • urlshortener_cache_hits_total / urlshortener_cache_misses_total - Cache effectiveness.
  • JVM metrics (GC, memory, threads) via Prometheus JVM instrumentation.

Fault Tolerance

The service is designed to gracefully degrade when external dependencies are unavailable:

  • Redis unavailable - Cache operations are skipped, all reads fall through to Datomic.
  • Kafka unavailable - Events are silently dropped. URL operations continue normally. Analytics will not be updated.
  • Datomic - Required for core operations. The service will not start without a valid connection.

Docker

Run the full stack

docker-compose up -d

This starts Redis, Kafka (KRaft mode), and the application on port 8080.

Run only infrastructure (for local development)

docker-compose up -d redis kafka

Then run the app locally with lein run.

Build the image separately

docker build -t url-shortener .

CI/CD

GitHub Actions pipeline runs on every push and PR to main:

  • Lint - clj-kondo static analysis.
  • Test - lein test on Java 11 and Java 17 matrix.
  • Coverage - lein coverage report uploaded as artifact.

Documentation

Document Description
ARCHITECTURE.md Diplomat Architecture specification and layer access rules
TESTING.md Testing guide, patterns and statistics
SETUP.md Prerequisites, getting started, API usage and configuration

About

Clojure URL shortener with Diplomat architecture, using Datomic for immutable URL storage & analytics, and Kafka for event streaming in real-time click processing. Features RESTful API with Ring, custom short codes, and production-grade distributed system design patterns.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages