A URL shortener service written in Go, built with a Domain-Driven Design (DDD) architecture. Uses raw net/http (no framework), PostgreSQL for persistence, Redis for caching and rate limiting, and RabbitMQ for async click analytics processing.
.
├── cmd → entrypoint & wiring
│ ├── bootstrap → bootstrap cli
│ │ ├── cmd
│ │ └── main.go
│ └── short → server
│ └── main.go
├── config → environment config parser
├── internal
│ ├── app → application layer (business logic)
│ │ └── url
│ │ ├── service.go
│ ├── cron → Cron cleaner
│ ├── domain → domain layer (entities, repository interfaces, domain errors)
│ │ ├── cache
│ │ ├── db
│ │ ├── queue
│ │ └── url
│ ├── infra → infrastructure layer
│ │ ├── postgres
│ │ │ ├── transaction_manager.go
│ │ ├── rabbitmq
│ │ └── redis
│ │ ├── cache
│ │ ├── rate_limitter
│ ├── transport → transposrt layer
│ │ └── http → HTTP server, middleware manager, route handlers
│ │ ├── handler
│ │ │ ├── static
│ │ │ │ ├── handler.go
│ │ │ │ └── routes.go
│ │ │ └── url
│ │ │ ├── handler.go
│ │ │ ├── routes.go
│ │ ├── middleware
│ │ │ ├── manager.go
│ │ └── server.go
│ ├── utils → helper functions (code generation, json response)
│ └── worker → async analytics consumer (RabbitMQ)
├── migrations → SQL migration files
├── tests → test files (k6 load test)
├── static → frontend code (claude generated)
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go.sum
├── .dockerignore
├── .env.example
├── .gitignore
└── README.md
| Concern | Technology |
|---|---|
| Language | Go 1.26 |
| HTTP | net/http (stdlib only, no framework) |
| Database | PostgreSQL (sqlx, lib/pq) |
| Migrations | golang-migrate |
| Cache | Redis (go-redis/v9) |
| Message Queue | RabbitMQ (amqp091-go) |
| Validation | go-playground/validator |
| ID Generation | google/uuid |
| User-Agent Parsing | mileusna/useragent |
- URL shortening — FNV-64 URL hash XOR'd into a UUID, base64url-encoded and truncated to 8 characters
- Redirect —
GET /{code}with Redis cache layer; expired URLs served from cache to avoid DB hits - Click analytics — async pipeline via RabbitMQ; captures browser, OS, device type, referrer, and timestamp per click
- Analytics endpoint —
GET /{code}/statreturns aggregated browser/OS/device breakdown and total click count - Rate limiting — token bucket implemented as an atomic Lua script in Redis
- Dead-letter queue — failed analytics messages are routed to
analytics.queue.deadafter exhausting retries - Graceful shutdown — SIGINT/SIGTERM handling with configurable drain timeout
- Cleaner - Cron job to delete expired urls
POST /short
Content-Type: application/json
{
"url": "https://example.com/some/long/path",
"expire_at": "2025-12-31T00:00:00Z" // optional
}
{
"msg": "success",
"code": "aB3kZ9",
"short_url": "http://localhost:3000/aB3kZ9",
"expire_at": "2025-12-31T00:00:00Z"
}GET /{code}
→ 302 redirect to original URL
GET /{code}/stat
{
"short": "aB3kZ9",
"total_count": 142,
"browser": { "Chrome": 98, "Firefox": 44 },
"device": { "Desktop": 120, "Mobile": 22 },
"os": { "Windows": 80, "macOS": 40, "Linux": 22 },
"expire_at": null
}sequenceDiagram
participant User
participant API as URL Service
participant MQ as RabbitMQ
participant Worker as Analytics Worker
participant DB as PostgreSQL
User->>API: GET /:code
API-->>User: 302 Redirect
API->>MQ: Publish ClickEvent (async)
MQ->>Worker: Consume ClickEvent
Worker->>Worker: Parse User-Agent
Note right of Worker: Detect browser,\ndevice, OS
Worker->>DB: Insert click record
Worker->>DB: Increment total clicks
Worker->>DB: Update last_clicked_at
alt Success
DB-->>Worker: OK
else Failure
Worker->>MQ: Retry (max 2x)
MQ->>Worker: Requeue event
alt Retry limit exceeded
Worker->>MQ: Dead Letter Queue
end
end
The worker runs with configurable concurrency (default 10) using a semaphore pattern, and uses manual acknowledgement (autoAck: false) so no clicks are lost on crash.
- Docker
Copy .env.example to .env and fill in your values:
ADDR=127.0.0.1
PORT=3000
PREFIX=http://localhost:3000/
SERVICE_NAME=short-api
PG_USER=shortuser
PG_PASSWORD=secret
PG_PORT=5432
PG_ADDRESS=localhost
PG_NAME=urlshortener
PG_SSLMODE=disable
PG_SUPERUSER=postgres
PG_SUPERDB=postgres
REDIS_ADDR=localhost:6379
RMQ_ADDR=localhost:5672
RMQ_USER=guest
RMQ_PASS=guestdocker compose up
docker compose up -d --build
services:
postgres: → PostgreSQL
redis: → Redis
rabbitmq: → RabbitMQ
bootstrap: → CLI to setup postgres, redis and rabbitmq
api: → API backend and frontend
Load tested with k6 using a constant-arrival-rate scenario mixing all three core endpoints (POST /short, GET /{code}, GET /{code}/stat) at realistic traffic proportions (80% reads, 20% writes).
Setup: 1000 req/s sustained for 60s, 150–500 max VUs, run via Docker Compose (API, Postgres, Redis, RabbitMQ, all containerized).
Run: shuf tests/get_urls.txt -o tests/get_urls.txt && k6 run -e RATE=1000 tests/load.js
With Rate Limiting Results (3 consecutive runs):
| Run | Throughput | p90 | p95 | Failed/unexpected | Dropped iterations |
|---|---|---|---|---|---|
| 1 | 992 req/s | 29.06ms | 67.93ms | 0.00% | 475 (0.80%) |
| 2 | 992 req/s | 46.41ms | 102.93ms | 0.00% | 365 (0.61%) |
| 3 | 995 req/s | 38.16ms | 80.69ms | 0.00% | 285 (0.48%) |
| Avg | 993 req/s | ~38ms | ~84ms | 0.00% | ~0.63% |
Without Rate Limiting Results (3 consecutive runs): Commenting rate limiter on '../transport/server.go' file
| Run | Throughput | p90 | p95 | Failed/unexpected | Dropped iterations |
|---|---|---|---|---|---|
| 1 | 981 req/s | 147.01ms | 248.89ms | 0.00% | 1109 (1.85%) |
| 2 | 970 req/s | 212.35ms | 335.7ms | 0.00% | 1787 (2.98%) |
| 3 | 920 req/s | 444.13ms | 691.12ms | 0.00% | 4605 (7.67%) |
| Avg | 957 req/s | ~267.8ms | ~425.2ms | 0.00% | ~4.17% |
Only 5xx codes are treated as a failure, others 4xx are valid as they are expected.
- Upgrade token bucket rate limiting to sliding window
- Query optimization