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.
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
- 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.inuses loose schemas (tolerant reader), whilewire.out,wire.cacheandwire.datomicuse 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.
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
| 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 |
- User Registration - Users register via
/api/auth/registerwith username and password (min 8 chars). Passwords are hashed with bcrypt+SHA512 and stored in Datomic. - JWT Authentication - Protected endpoints require a
Bearertoken obtained via/api/auth/loginwith 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-Afterheader when rate limit is exceeded.
/metricsendpoint 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.
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-compose up -dThis starts Redis, Kafka (KRaft mode), and the application on port 8080.
docker-compose up -d redis kafkaThen run the app locally with lein run.
docker build -t url-shortener .GitHub Actions pipeline runs on every push and PR to main:
- Lint - clj-kondo static analysis.
- Test -
lein teston Java 11 and Java 17 matrix. - Coverage -
lein coveragereport uploaded as artifact.
| 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 |
