Tiny, deterministic, cache-friendly URL shortener with expirable links.
TOC: 🚀 Quick Start • 🔧 Usage • 🧱 Architecture & Design • ⚙️ Configuration • 🗄️ Database & Migrations • 🐳 Container Image • 🧪 Testing • 🔐 Security Notes • 📜 License
Prereqs: Java 21, Maven (wrapper provided), Docker (for Postgres).
Run Postgres:
docker-compose up -dRun the app (dev profile):
./mvnw spring-boot:run -Dspring-boot.run.profiles=devDev defaults:
- URL: http://localhost:4010/
- API Key: 1234567890
- DB: localhost:29332 (user/pass: bihan / bihan)
Jar build:
./mvnw clean package
java -jar target/bihan-0.0.1-SNAPSHOT.jar --spring.profiles.active=devCreate a short link (only admin operation exposed):
API_KEY=1234567890
BASE=http://localhost:4010
curl -s -X POST "$BASE/api/links" \
-H "Authorization: ApiKey $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://breizhcamp.org/schedule",
"expirationDate": "2025-12-31T23:59:59Z",
"id": "optionalCustomId"
}'
# => {"id":"AbCdEf"}Follow redirect:
open $BASE/AbCdEf # or curl -I $BASE/AbCdEf
Behavior:
- Unknown / expired / missing ID → redirects to fallback (
bihan.base-redirect) - Same (url, expirationDate) submitted again returns same short code
- Optional
idused if free; else random 6-letter (a–zA–Z)
Hexagonal / layered slices:
application/ (REST controllers, cron, DTO)
config/ (typed properties)
domain/ (entity + use cases + ports)
infrastructure/ (adapters, JPA repo, persistence model)
Flow (create):
AdminCtrl → AddLink use case → LinkPort → LinkAdapter → LinkRepo (JPA) → Postgres
Flow (redirect): RedirectCtrl → GetLink → cache / DB → URL or fallback
Key points ✨:
- Deterministic persistence id: SHA-256(expiration|url) prevents duplicates
- Public short code (
linkId) either user-provided or generated (collision-checked) - Expiration enforced at read + periodic purge
- Caffeine cache (
@Cacheable("links")) for read path - Hourly purge (
PurgeLinksCron) evicts expired rows + clears cache - Liquibase migrations for schema
- Small surface area (only one write endpoint) → simple hardening
ID Strategy:
- DB primary key = hash (stable)
- Short code = human friendly (6 chars) or explicit
- Up to 1000 attempts to generate a collision-free random code
Caching:
- GET lookups cached by short code
- Mutations (add / purge) evict as needed
Prefix bihan. (see BihanConfig):
| Property | Description |
|---|---|
| base-redirect | Fallback target when ID invalid / expired / absent |
| public-url | Public base URL of this instance |
| api-key | Shared secret for POST /api/links |
Dev profile (application-dev.yml):
server.port=4010- Local Postgres config
bihan.api-key=1234567890
Activate a profile:
-Dspring.profiles.active=dev
# or
SPRING_PROFILES_ACTIVE=dev
Env-based prod example:
spring:
datasource:
url: jdbc:postgresql://${POSTGRESQL_ADDON_HOST}:${POSTGRESQL_ADDON_PORT}/${POSTGRESQL_ADDON_DB}
username: ${POSTGRESQL_ADDON_USER}
password: ${POSTGRESQL_ADDON_PASSWORD}
bihan:
base-redirect: https://www.breizhcamp.org/
public-url: https://bzh.camp/
api-key: ${ADMIN_API_KEY}- Postgres (see
docker-compose.yml) - Liquibase changelogs under
src/main/resources/db/changelog - Auto-applied on startup
- Purge job physically deletes expired links hourly
Build local Docker image via Jib (no Dockerfile needed):
./mvnw jib:dockerBuild -Dimage=bihan:localPush to registry:
./mvnw jib:build -Dimage=ghcr.io/your-org/bihan:latest./mvnw test- Simple header auth:
Authorization: ApiKey <key> - 404 on auth failure (avoids differentiating auth vs not-found)
- Recommend TLS termination + rate limiting at reverse proxy
- Consider rotation of API key through secret management
Bihan is free and open-source software licensed under the GPL-3.0 License
Enjoy shortening! 🚀