A high-performance URL shortener service built with Go, using Cassandra for persistent storage and Redis for counter management. This service provides a RESTful API to create short URLs and retrieve the original URLs.
- 🔗 URL Shortening: Convert long URLs into short, memorable codes
- 🔄 URL Retrieval: Get the original URL from a short code
- 📊 Counter Management: Uses Redis for distributed counter management
- 💾 Persistent Storage: Cassandra for reliable, scalable data storage
- 🚀 High Performance: Optimized database queries with partition key design
- 📝 API Documentation: Swagger/OpenAPI documentation in development mode
- 🏥 Health Checks: Built-in health check endpoint
- 🧪 Test Coverage: Comprehensive test suite with isolated test databases
- 🔍 Observability: OpenTelemetry integration for distributed tracing
- ⚖️ Load Balancing: Nginx load balancer with 4 app replicas
- 🔄 HAProxy: Cassandra load balancing for high availability
The project follows a clean architecture pattern with clear separation of concerns:
- Domain Layer: Business logic and entities
- Gateway Layer: External integrations (HTTP, Cassandra)
- Extension Layer: Infrastructure utilities (config, logger, Redis, OpenTelemetry)
┌─────────────┐
│ Frontend │ → http://localhost:8888
└──────┬──────┘
│
┌──────▼──────┐
│ Nginx │ (Port 8888) - Load Balancer
└──────┬──────┘
│
┌──────▼──────┐
│ App (x4) │ (Load balanced replicas)
└──────┬──────┘
┌───┴──┐
┌──▼──┐ ┌─▼──┐
│Redis│ │ DB │
└─────┘ └──┬─┘
│
┌────▼────┐
│HAProxy │ (Cassandra Load Balancer)
└────┬────┘
│
┌────▼────┐
│Cassandra│
└─────────┘
- Go 1.24 or higher
- Docker and Docker Compose (for running all services)
- Make (optional, for using Makefile commands)
- Clone the repository:
git clone [email protected]:eulixir/lnk.git
cd lnk- Install backend dependencies:
cd backend
go mod download- Copy
.env.exampleto.envfile in thebackenddirectory with the following configuration:
# Cassandra
CASSANDRA_HOST=cassandra-lb
CASSANDRA_PORT=9042
CASSANDRA_USERNAME=cassandra
CASSANDRA_PASSWORD=cassandra
CASSANDRA_KEYSPACE=lnk
CASSANDRA_AUTO_MIGRATE=true
# APP
ENV=development
PORT=8080
GIN_MODE=debug
BASE62_SALT=banana
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
COUNTER_KEY=short_url_counter
COUNTER_START_VAL=14000000
# Log
LOG_LEVEL=debug
# Otel
SERVICE_NAME=lnk-backend
OTEL_EXPORTER_OTLP_ENDPOINT=grafana:4317Note:
- Make sure to set a secure
BASE62_SALTvalue in production - Use Docker service names (e.g.,
cassandra-lb,redis,grafana) when running in Docker - Use
localhostwhen running services locally outside Docker
- Start all services using Docker Compose:
cd backend
docker-compose up -dThis will start all services. See Docker Services section for details.
All services are managed via Docker Compose:
cd backend
docker-compose up -d # Start all services
docker-compose logs -f app # View app logs
docker-compose down # Stop all servicesThe API will be available at http://localhost:8888 (via Nginx load balancer).
If you want to run the application locally outside Docker:
- Update
.envto uselocalhostinstead of Docker service names:
CASSANDRA_HOST=localhost
REDIS_HOST=localhost
OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317- Start only the infrastructure services:
cd backend
docker-compose up -d cassandra redis grafana- Run the application:
cd backend
go run cmd/app/main.goThe server will start on http://localhost:8080 (or the port specified in your .env file).
GET /health
Check if the API is running.
Response:
{
"message": "OK"
}POST /shorten
Create a short URL from a long URL.
Request Body:
{
"url": "https://www.example.com/very/long/url/path"
}Response:
{
"short_url": "abc123",
"original_url": "https://www.example.com/very/long/url/path"
}GET /{short_url}
Retrieve the original URL from a short code.
Response:
{
"url": "https://www.example.com/very/long/url/path"
}Status Codes:
200: Success308: Permanent Redirect (original URL found)404: URL not found500: Internal server error
In development mode, Swagger documentation is available at:
http://localhost:8888/swagger/index.html
cd backend
make test # Run tests
make coverage # Generate coverage reportTests use isolated test databases that are automatically created and cleaned up.
lnk/
├── backend/
│ ├── cmd/
│ │ ├── app/
│ │ │ └── main.go # Application entry point
│ │ └── migrator/
│ │ └── main.go # Migration service
│ ├── domain/ # Domain layer
│ │ └── entities/
│ │ ├── helpers/ # URL encoding/decoding utilities
│ │ └── usecases/ # Business logic
│ ├── gateways/ # Gateway layer
│ │ ├── gocql/ # Cassandra integration
│ │ │ ├── migrations/ # Database migrations
│ │ │ └── repositories/ # Data access layer
│ │ └── http/ # HTTP handlers and router
│ ├── extensions/ # Infrastructure extensions
│ │ ├── config/ # Configuration management
│ │ ├── logger/ # Logging utilities
│ │ ├── redis/ # Redis client
│ │ └── opentelemetry/ # OpenTelemetry setup
│ ├── nginx/
│ │ └── nginx.conf # Nginx load balancer config
│ ├── haproxy/
│ │ └── haproxy.cfg # HAProxy Cassandra LB config
│ ├── docker-compose.yml # Docker services configuration
│ ├── Dockerfile # Multi-stage build (migrator + app)
│ ├── Makefile # Build automation
│ └── .env # Environment configuration
├── frontend/ # Next.js frontend application
└── Readme.md # This file
The urls table uses short_code as the partition key for optimal query performance:
CREATE TABLE urls (
short_code TEXT,
long_url TEXT,
created_at TIMESTAMP,
PRIMARY KEY (short_code)
);This design ensures fast lookups when retrieving URLs by their short code.
The frontend is a modern Next.js application that provides a user-friendly interface for the URL shortener service.
- 🎨 Modern UI: Built with Next.js 16 and React 19
- 🎯 Type-Safe API Client: Auto-generated TypeScript client from Swagger/OpenAPI
- 🎨 Beautiful Components: Uses shadcn/ui component library
- 📱 Responsive Design: Mobile-friendly interface
- ⚡ Fast Performance: Optimized with React Compiler
- Node.js 18+ or Bun
- Backend API running (see Backend section)
- Navigate to the frontend directory:
cd frontend- Install dependencies:
# Using npm
npm install
# Or using Bun
bun install- Generate API client from Swagger documentation:
npm run generate:api
# Or
bun run generate:apiNote: Make sure the backend is running and Swagger documentation is available at http://localhost:8888/swagger/doc.json before generating the API client.
npm run dev
# Or
bun run devThe frontend will start on http://localhost:3000 (default Next.js port).
Important: Update the API base URL in frontend/src/api/undici-instance.ts to point to http://localhost:8888 (Nginx load balancer).
npm run build
npm run start
# Or
bun run build
bun run startnpm run dev/bun run dev: Start development servernpm run build/bun run build: Build for productionnpm run start/bun run start: Start production servernpm run lint/bun run lint: Run linter (Biome)npm run lint:fix/bun run lint:fix: Fix linting issuesnpm run format/bun run format: Format codenpm run generate:api/bun run generate:api: Generate API client from Swagger
frontend/
├── src/
│ ├── app/ # Next.js App Router pages
│ │ ├── [shortUrl]/ # Dynamic route for URL redirection
│ │ └── page.tsx # Home page
│ ├── api/ # API client and configuration
│ │ ├── lnk.ts # Auto-generated API client
│ │ └── undici-instance.ts # Custom fetch instance
│ ├── components/ # React components
│ │ ├── ui/ # shadcn/ui components
│ │ ├── url-dialog.tsx
│ │ ├── url-input.tsx
│ │ └── url-shortener.tsx
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utility functions
│ └── types/ # TypeScript type definitions
├── public/ # Static assets
├── orval.config.ts # API client generation config
├── next.config.ts # Next.js configuration
└── package.json # Dependencies and scripts
- Go 1.24: Programming language
- Gin: HTTP web framework
- Cassandra (gocql): Database for URL storage
- Redis: Counter management for URL generation
- Zap: Structured logging
- OpenTelemetry: Distributed tracing
- Swagger/OpenAPI: API documentation
- Docker Compose: Local development environment
- Nginx: Load balancer
- HAProxy: Cassandra load balancer
- Testify: Testing framework
- Next.js 16: React framework
- React 19: UI library
- TypeScript: Type safety
- Tailwind CSS: Styling
- shadcn/ui: Component library
- Orval: API client generator
cd backend
# Testing
make test # Run tests
make coverage # Generate test coverage report
# Documentation
make swagger # Generate Swagger documentation
# Migrations
make generate-migration NAME=your_migration_name # Create new migration
# Migrations auto-run via migrator service when using docker-compose
# Code Generation
make generate # Generate all (swagger + mocks)Migrations run automatically via the migrator service before the app starts. To add a new migration:
cd backend
make generate-migration NAME=your_migration_nameThis creates migration files in gateways/gocql/migrations/ that will run automatically on next docker-compose up.
| Service | Port | Description |
|---|---|---|
| Nginx | 8888 | Load balancer (frontend access point) |
| App | 8080 | Application service (4 replicas, internal) |
| Migrator | - | Runs database migrations, then exits |
| HAProxy | 9042 | Cassandra load balancer |
| Cassandra | - | Primary database (accessed via HAProxy) |
| Redis | 6379 | Counter management and caching |
| Grafana | 8081, 4317 | Observability UI (8081) and OTLP gRPC (4317) |
Services start in the following order with health checks:
- Cassandra → waits until healthy (
nodetool status) - HAProxy → waits for Cassandra, checks with
pgrep haproxy - Redis → checks with
redis-cli ping - Grafana → observability stack
- Migrator → waits for Cassandra, runs migrations, exits
- App → waits for migrator completion, 4 replicas, HTTP health check
- Nginx → waits for app service
Check service status:
docker-compose psOpenTelemetry traces are automatically exported to Grafana:
- OTLP Endpoint:
grafana:4317(gRPC) when running in Docker - Grafana UI:
http://localhost:8081(admin/admin) - Service Name:
lnk-backend(configurable viaSERVICE_NAMEenv var)
Check service logs:
docker-compose logs -f [service-name]Check service status:
docker-compose psCheck migrator logs:
docker-compose logs migratorEnsure Cassandra is healthy:
docker-compose exec cassandra nodetool status- Verify service names in
.envmatch Docker service names - Check that services are on the same Docker network (
lnk-network) - Verify ports are not already in use
- Ensure backend is running:
http://localhost:8888/health - Check CORS configuration in
gateways/http/middleware/middleware.go - Verify API base URL in frontend configuration