A full-stack URL shortener built to learn and practice system design concepts hands-on. The project implements core distributed systems patterns β URL shortening with Base62 encoding, rate limiting with a sliding window algorithm, and round-robin load balancing β all in a clean Spring Boot + React stack.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BROWSER β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β React Frontend (localhost:3000) β β
β β ββββββββββββββββ ββββββββββββββ βββββββββββββββββββββ β β
β β β ShorternForm β β ResultCard β β LinkHistory β β β
β β ββββββββ¬ββββββββ ββββββββββββββ βββββββββββββββββββββ β β
β β β β² β² β β
β β βΌ β β β β
β β βββββββββββββββββββββββ΄βββββββββββββββββββββ β β
β β β urlService.js (axios) β β
β β β POST /api/urls/shorten GET /api/urls β β
β β ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ β
β β CORS β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββ
β Spring Boot Backend (localhost:8080) β
β β β
β βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββ β
β β RateLimiterInterceptor β β
β β (Sliding Window β 5 req/min per IP) β β
β β Returns HTTP 429 if limit exceeded β β
β βββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββ β
β β UrlController β β
β β POST /api/urls/shorten β GET /api/urls β GET /{shortId} (302) β β
β β DELETE /api/urls/{id} β β β β
β βββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββ β
β β UrlService β β
β β shortenUrl() β getAllUrls() β getOriginalAndTrackClick() β β
β β β β (increments clickCount) β β
β ββββββββββ¬ββββββββ΄βββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββΌββββββββββ βββββββββββββββββββ β
β β Base62Util β β UrlRepository β β
β β (ID β shortCode) β β (JPA/SQLite) β β
β ββββββββββββββββββββ ββββββββββ¬βββββββββ β
β β β
β βββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ β
β β SQLite (urlshortner.db) β β
β β url_mapping: id | long_url | short_id | click_count | created_at β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React 19, Axios, Tailwind CSS | SPA for shortening URLs and viewing history |
| Backend | Spring Boot 4.0, Java 17 | REST API, business logic, CORS handling |
| Database | SQLite (via JPA/Hibernate) | Persistent storage for URL mappings |
| Encoding | Base62 (custom utility) | Converts auto-increment DB IDs to short codes |
The core algorithm converts a database auto-increment ID into a short alphanumeric string:
ID = 1 β encode(1) β "b"
ID = 62 β encode(62) β "ba"
ID = 238328 β encode(238328) β "bzzz"
Why Base62? β Uses a-z, A-Z, 0-9 (62 chars). A 7-character code supports 62β· = 3.5 trillion unique URLs. No special characters, so codes are URL-safe and copy-paste friendly.
Custom alias support β Users can optionally provide their own alias (e.g., my-site) instead of the auto-generated code.
Every incoming request passes through a RateLimiterInterceptor before reaching any controller:
Request β [RateLimiterInterceptor] β Allow? β Controller
β
ββ Tracks timestamps per IP in a ConcurrentHashMap
ββ Window: 60 seconds, Limit: 5 requests
ββ Exceeds? β HTTP 429 "Too Many Requests"
Algorithm: Sliding Window Log β stores exact request timestamps and removes expired ones on each check. Thread-safe via ConcurrentHashMap + Collections.synchronizedList.
A simple round-robin load balancer distributes requests across multiple backend instances:
Request 1 β localhost:8081
Request 2 β localhost:8082
Request 3 β localhost:8083
Request 4 β localhost:8081 (cycles back)
Uses AtomicInteger for thread-safe index rotation.
Configured in CorsConfig.java to allow the React dev server (port 3000) to call the Spring Boot API (port 8080):
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*");| Method | Endpoint | Description | Request Body | Response |
|---|---|---|---|---|
POST |
/api/urls/shorten |
Shorten a URL | {"originalUrl": "...", "alias": "..."} |
UrlResponse JSON |
GET |
/api/urls |
Get all shortened URLs | β | UrlResponse[] |
DELETE |
/api/urls/{id} |
Delete a URL by ID | β | 204 No Content |
GET |
/{shortId} |
Redirect to original URL | β | 302 Redirect |
{
"id": 1,
"originalUrl": "https://google.com",
"shortUrl": "http://localhost:8080/b",
"shortCode": "b",
"clickCount": 3,
"createdAt": "2026-02-21T00:31:35.092"
}SystemDesignURLShortner/
βββ URLShortner/ # Spring Boot Backend
β βββ src/main/java/com/systemdesig/
β βββ SystemDesignApplication.java # Entry point
β βββ config/
β β βββ CorsConfig.java # CORS policy
β β βββ WebConfig.java # Interceptor registration
β β βββ RateLimiterInterceptor.java # Pre-request rate check
β βββ urlshortner/
β β βββ UrlController.java # REST endpoints
β β βββ UrlService.java # Business logic
β β βββ UrlRepository.java # JPA data access
β β βββ UrlMapping.java # Entity (DB table)
β β βββ ShortenRequest.java # Request DTO
β β βββ UrlResponse.java # Response DTO
β βββ ratelimiter/
β β βββ RateLimitController.java # Rate limit test endpoint
β β βββ RateLimiterService.java # Sliding window algorithm
β βββ loadbalancer/
β β βββ LoadBalancerController.java
β β βββ LoadBalancerService.java # Round-robin selector
β βββ common/util/
β βββ Base62Util.java # ID β short code encoding
β
βββ url_frontend/ # React Frontend
βββ src/
βββ App.jsx # Main app with history + form
βββ api/urlService.js # Axios HTTP client
βββ components/
βββ ShorternForm.jsx # URL input + shorten button
βββ ResultCard.jsx # Displays shortened URL + copy
βββ LinkHistory.jsx # Table of past shortened URLs
βββ Navbar.jsx # Top navigation bar
- Java 17+
- Maven 3.8+
- Node.js 18+
cd URLShortner
mvn spring-boot:run
# Starts on http://localhost:8080cd url_frontend
npm install
npm start
# Opens http://localhost:3000# Shorten a URL
curl -X POST http://localhost:8080/api/urls/shorten \
-H "Content-Type: application/json" \
-d '{"originalUrl":"https://github.com"}'
# Get history
curl http://localhost:8080/api/urls
# Test redirect (use the shortCode from the shorten response)
curl -v http://localhost:8080/b| Change | Why |
|---|---|
| Replace SQLite β PostgreSQL | SQLite doesn't support concurrent writes well; PSQL handles production workloads |
| Add Flyway migrations | Version-controlled schema changes instead of ddl-auto=update |
| Add connection pooling config | HikariCP tuning for production connection management |
| Change | Why |
|---|---|
| Move rate limiter state to Redis | In-memory ConcurrentHashMap is lost on restart and doesn't work across multiple server instances |
Use Redis SORTED SET for sliding window |
ZADD + ZRANGEBYSCORE gives O(log n) sliding window with automatic expiry |
| Add Redis cache for hot short URLs | Avoid DB lookups for frequently accessed short codes; huge latency reduction |
| TTL-based cache eviction | Auto-expire cache entries for rarely used URLs |
| Change | Why |
|---|---|
| Distributed ID generation (Twitter Snowflake / UUID) | Auto-increment IDs don't scale across multiple DB instances |
| Real load balancer (Nginx / Spring Cloud Gateway) | Replace the demo round-robin class with infrastructure-level load balancing |
| Multiple backend instances | Run 2-3 Spring Boot instances behind the load balancer |
| Database read replicas | Separate read traffic (redirects) from write traffic (shortening) |
| Change | Why |
|---|---|
| Click analytics dashboard | Track clicks by time, geography, referrer |
| Store click events in a time-series DB (InfluxDB / TimescaleDB) | Efficient storage for high-volume event data |
| Add Prometheus + Grafana monitoring | Track API latency, error rates, throughput |
| Structured logging (ELK stack) | Centralized log collection and searchability |
| Change | Why |
|---|---|
| User authentication (JWT / OAuth2) | Associate URLs with user accounts |
| URL validation & sanitization | Block malicious URLs, validate format |
| Custom domain support | Let users use their own domain for short links |
| Expiry / TTL for short URLs | Auto-delete URLs after a configurable period |
| Restrict CORS to specific origins | allowedOrigins("*") is fine for dev, not for production |
ββββββββββββ
β Client β
ββββββ¬ββββββ
β
ββββββΌββββββ
β Nginx β (Load Balancer + SSL Termination)
ββββββ¬ββββββ
β
ββββββββββββββΌβββββββββββββ
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
β App Srv 1β β App Srv 2β β App Srv 3β (Spring Boot instances)
ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ
β β β
βββββββββββββββΌβββββββββββββ
β
ββββββββββββΌβββββββββββ
βΌ βΌ
ββββββββββββ ββββββββββββ
β Redis β β Postgres β
β (Cache + β β (Primary β
β Rate β β + Read β
β Limit) β β Replicas)β
ββββββββββββ ββββββββββββ
Built for learning system design β one component at a time. π