A robust URL shortening service built with Spring Boot 3.2.3, featuring Redis caching, PostgreSQL persistence, and comprehensive error handling.
- URL Shortening: Generate short codes for long URLs using Base62 encoding
- Custom Short Codes: Allow users to specify custom short codes
- Click Tracking: Track usage statistics for each shortened URL
- URL Expiration: Automatic URL expiration after configurable days (default: 30)
- Redis Caching: Improve performance with Redis caching for URL lookups
- Collision Handling: Automatic retry logic for short code generation
- Structured Logging: Comprehensive logging using SLF4J
- Health Monitoring: Spring Boot Actuator endpoints for health checks and metrics
- Docker Support: Complete Docker Compose setup for easy deployment
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │────▶│ Controller │────▶│ Service │
└─────────────┘ └──────────────┘ └──────┬──────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Redis │ │PostgreSQL│ │ Logs │
└─────────┘ └─────────┘ └─────────┘
- Java 17
- Spring Boot 3.2.3
- Gradle 8.5 (Groovy)
- PostgreSQL 15
- Redis 7
- JUnit 5 + Mockito for testing
docker-compose upThis will start:
- PostgreSQL on port 5432
- Redis on port 6379
- Spring Boot application on port 8080
-
Prerequisites
- Java 17
- PostgreSQL 15
- Redis 7
-
Database Setup
CREATE DATABASE urlshortener;
-
Build the project
./gradlew build
-
Run the application
./gradlew bootRun
POST /api/shorten
Request Body:
{
"url": "https://example.com/very-long-url",
"customCode": "mylink" // optional
}Response (201 Created):
{
"data": {
"shortCode": "abc123",
"originalUrl": "https://example.com/very-long-url"
},
"success": true
}GET /api/{shortCode}
Response (302 Found):
- Redirects to the original URL via Location header
GET /api/stats/{shortCode}
Response (200 OK):
{
"data": {
"shortCode": "abc123",
"originalUrl": "https://example.com/very-long-url",
"createdAt": "2026-03-15T10:00:00",
"clickCount": 42
},
"success": true
}GET /actuator/health
Response (200 OK):
{
"status": "UP"
}GET /actuator/metrics
Response (200 OK):
{
"names": ["jvm.memory.used", "http.server.requests", ...]
}| Variable | Description | Default |
|---|---|---|
SERVER_PORT |
Application port | 8080 |
DB_URL |
PostgreSQL JDBC URL | jdbc:postgresql://localhost:5432/urlshortener |
DB_USERNAME |
Database username | postgres |
DB_PASSWORD |
Database password | postgres |
REDIS_HOST |
Redis host | localhost |
REDIS_PORT |
Redis port | 6379 |
URL_EXPIRATION_DAYS |
URL expiration in days | 30 |
JPA_SHOW_SQL |
Show SQL queries in logs | true |
- Uses alphanumeric characters (0-9, A-Z, a-z)
- Provides 62^6 ≈ 56 billion possible combinations for 6-character codes
- URL-safe and human-readable
- Reduces database load for frequently accessed URLs
- Improves response time for redirects
- Simple key-value storage perfect for URL lookups
- 302 (Found) is temporary - allows analytics tracking
- 301 (Moved Permanently) would be cached by browsers, preventing accurate click counting
- 302 is more appropriate for a URL shortener service
- Ensures atomicity when incrementing click counts
- Prevents race conditions during concurrent redirects
- Guarantees data consistency
Run unit tests:
./gradlew testRun with coverage:
./gradlew test jacocoTestReportsrc/
├── main/
│ ├── java/com/urlshortener/
│ │ ├── controller/ # REST controllers
│ │ ├── dto/ # Data Transfer Objects
│ │ ├── entity/ # JPA entities
│ │ ├── exception/ # Custom exceptions
│ │ ├── repository/ # JPA repositories
│ │ └── service/ # Business logic
│ └── resources/
│ └── application.properties
└── test/
└── java/com/urlshortener/
└── service/ # Unit tests
The API returns structured error responses:
{
"error": "Short code not found: invalid",
"status": 404,
"timestamp": "2026-03-15T10:00:00"
}HTTP Status Codes:
201- URL shortened successfully302- Redirect to original URL400- Invalid request (validation error)404- Short code not found409- Custom code already taken410- URL has expired503- Service unavailable (short code generation failure)500- Internal server error
This project is for educational purposes.