A URL shortening service built with Spring Boot and MongoDB Atlas Local. It stores long URLs, generates collision-safe short IDs, redirects users, and tracks redirect analytics.
Stack: Java 25, Spring Boot, Spring Data MongoDB, MongoDB Atlas Local, Swagger/OpenAPI, JUnit 5, Mockito
- Quick Start
- Runtime Overview
- API Reference
- Data Model
- Collision-safe ID Strategy
- Configuration
- Tests
The module now includes a Docker Compose file for the local MongoDB Atlas Local runtime.
Start local MongoDB with:
docker compose -f linker/compose.yaml up -dStop it with:
docker compose -f linker/compose.yaml downIf you want to clear the persisted local MongoDB data too:
docker compose -f linker/compose.yaml down -vThis stack exposes MongoDB Atlas Local on localhost:27017 and uses container name
linker-mongodb by default.
Local Docker details:
| Setting | Value |
|---|---|
| Image | mongodb/mongodb-atlas-local:8 |
| Host port | 127.0.0.1:27017 |
| Volumes | linker_mongodb_data, linker_mongodb_config |
| Healthcheck | /usr/local/bin/runner healthcheck |
mvn -pl linker spring-boot:run| URL | Description |
|---|---|
| http://localhost:8100/swagger-ui/index.html | Swagger UI |
| http://localhost:8100/v3/api-docs | OpenAPI JSON |
For the detailed runtime/data-flow view, see ARCHITECTURE.md.
linker is a single Spring Boot module with three main runtime layers:
LinkControllerexposes the short-link, redirect, analytics, and list endpoints.LinkServiceowns short-code creation, idempotent URL reuse, expiration checks, and redirect-count updates.LinkRepositoryprovides MongoDB persistence forLinkdocuments.
The redirect flow is:
- look up the short URL
- return
404if missing - return
410if expired - increment analytics fields
- respond with
302 Foundand aLocationheader
The create flow is intentionally idempotent:
- look up the earliest stored link for the incoming full URL
- return that short URL immediately if it already exists
- otherwise generate a candidate short code
- retry on collisions or concurrent duplicate-key races
- persist and return the final short URL
Base path: /api/v1/links
| Method | Path | Description | Success response |
|---|---|---|---|
PUT |
/ |
Create or retrieve a short link | 200 plain-text short URL |
GET |
/{shortUrl} |
Redirect to original URL | 302 Found + Location header |
GET |
/{shortUrl}/analytics |
Get redirect analytics | 200 LinkAnalytics JSON |
GET |
/ |
List all stored links | 200 List<Link> JSON |
Request body: raw URL string (plain text or JSON string in Swagger).
https://example.org/very/long/path?with=params
Response: full short URL as plain text.
http://localhost:8100/api/v1/links/Ab12Cd
Idempotent: submitting the same URL a second time returns the existing short link. No duplicate document is created for that URL.
Implementation note:
LinkControlleraccepts both plain text and JSON-string request bodies and normalizes wrapped quotes before delegating toLinkService.
| Condition | Status |
|---|---|
| Link found and not expired | 302 Found + Location header pointing to original URL |
| Unknown short code | 404 Not Found |
| Link past its expiration date | 410 Gone |
Returns 404 Not Found for an unknown short code, otherwise:
{
"shortUrl": "Ab12Cd",
"url": "https://example.org/page",
"creationDate": "2026-03-11T11:12:00.000+00:00",
"expirationDate": "2026-04-10T11:12:00.000+00:00",
"redirectCount": 3,
"lastAccessDate": "2026-03-11T11:14:21.000+00:00",
"expired": false
}Returns the raw stored Link documents from MongoDB as JSON.
Link — MongoDB document stored in collection link.
| Field | Type | Description |
|---|---|---|
id |
String | MongoDB ObjectId |
url |
String | Original long URL |
shortUrl |
String | Unique short code (unique index) |
creationDate |
Date | Creation timestamp |
expirationDate |
Date | Expiration timestamp |
redirectCount |
long | Number of successful redirects |
lastAccessDate |
Date | Timestamp of the most recent redirect |
Short codes are random alphanumeric strings (default: 6 characters, charset A-Z a-z 0-9). The service handles both pre-insert collisions and concurrent-insert races:
flowchart TD
A[Generate random ID] --> B{existsByShortUrl?}
B -- yes --> C{attempts < maxAttempts?}
C -- yes --> A
C -- no --> ERR[throw IllegalStateException]
B -- no --> INS[INSERT Link document]
INS --> DKE{DuplicateKeyException?}
DKE -- same URL inserted concurrently --> RET_EXIST[return existing short URL]
DKE -- different collision race --> A
DKE -- no exception --> RET_NEW[return new short URL]
- Pre-insert check:
existsByShortUrl()detects known collisions before the write. - Unique index on
shortUrl: prevents duplicates at the database level. - Post-insert guard: catches
DuplicateKeyExceptionfor concurrent insert races.
host: http://localhost:8100/api/v1/links
linker:
short-url:
length: 6
max-attempts: 64
expiration-days: 30
spring:
data:
mongodb:
database: links
uri: mongodb://localhost:27017/?directConnection=true/linksThe local Docker setup in compose.yaml matches that default local MongoDB port.
The key runtime properties are:
| Property | Meaning |
|---|---|
host |
base URL prefix used when building the returned short URL |
linker.short-url.length |
generated short-code length |
linker.short-url.max-attempts |
maximum retry count before failing URL creation |
linker.expiration-days |
default lifetime of a created link |
spring.data.mongodb.uri |
MongoDB connection string |
Run the full test suite for this module:
mvn -pl linker testIn-memory MongoDB for tests is provided by mongo-java-server, so the module tests do not require
the local Docker MongoDB container.
The test mix covers:
- controller behavior
- link creation and redirect logic
- repository integration
- short-code generation rules