A production-ready URL shortener service built with Node.js, demonstrating key Low-Level Design patterns and SOLID principles.
This project showcases a complete URL shortener implementation following industry-standard design patterns including Strategy Pattern, Repository Pattern, Dependency Injection, and Layered Architecture.
- β URL shortening with customizable strategies
- β Redis caching for high performance
- β Click tracking and analytics
- β MongoDB for persistent storage
- β RESTful API design
- β Extensible architecture
The application follows a clear separation of concerns with distinct layers:
βββββββββββββββββββββββββββββββββββββββ
β Routes Layer β β HTTP endpoints
βββββββββββββββββββββββββββββββββββββββ€
β Controller Layer β β Request handling
βββββββββββββββββββββββββββββββββββββββ€
β Service Layer β β Business logic
βββββββββββββββββββββββββββββββββββββββ€
β Repository Layer β β Data access
βββββββββββββββββββββββββββββββββββββββ€
β Model Layer + Database β β Data persistence
βββββββββββββββββββββββββββββββββββββββ
Benefits:
- Clear separation of concerns
- Easy to test individual layers
- Maintainable and scalable code
The Strategy Pattern allows different algorithms for generating short codes to be swapped at runtime without modifying the service logic.
Base Strategy (Interface):
// src/strategies/shortCode.strategy.js
class ShortCodeStrategy {
generate() {
throw new Error("generate() must be implemented");
}
}Concrete Strategy:
// src/strategies/base62.strategy.js
class Base62Strategy extends ShortCodeStrategy {
generate() {
return nanoid(7); // Generates URL-safe short codes
}
}Usage in Service:
class UrlService {
constructor(shortCodeStrategy) {
this.shortCodeStrategy = shortCodeStrategy; // Strategy injection
}
async shorten(originalUrl) {
const shortCode = this.shortCodeStrategy.generate(); // Uses injected strategy
// ... rest of logic
}
}Benefits:
- Open/Closed Principle: Open for extension, closed for modification
- Easy to add new short code generation algorithms (MD5, Base62, Sequential, etc.)
- No need to modify existing service code when adding new strategies
Example: Adding a New Strategy
// src/strategies/sequential.strategy.js
class SequentialStrategy extends ShortCodeStrategy {
constructor() {
super();
this.counter = 1000;
}
generate() {
return `URL${this.counter++}`;
}
}
// Usage in routes
const service = new UrlService(new SequentialStrategy());Abstracts data access logic from business logic, making the codebase database-agnostic.
// src/repositories/url.repository.js
class UrlRepository {
async create(data) {
return Url.create(data);
}
async findByCode(code) {
return Url.findOne({ shortCode: code });
}
async incrementClicks(code) {
return Url.updateOne(
{ shortCode: code },
{ $inc: { clickCount: 1 } }
);
}
}Benefits:
- Centralizes all database queries
- Easy to switch databases (MongoDB β PostgreSQL)
- Simplifies unit testing with mock repositories
- Clear separation between data access and business logic
Dependencies are injected rather than hard-coded, improving testability and flexibility.
// Routes file - Dependency injection at startup
const service = new UrlService(new Base62Strategy());
const controller = new UrlController(service);Benefits:
- Loose coupling between components
- Easy to test (inject mock dependencies)
- Flexible configuration (swap implementations easily)
Implements cache-aside pattern with Redis for optimal performance:
async resolve(shortCode) {
// 1. Try cache first
const cachedUrl = await redisClient.get(shortCode);
if (cachedUrl) {
await urlRepository.incrementClicks(shortCode);
return cachedUrl;
}
// 2. Cache miss - query database
const url = await urlRepository.findByCode(shortCode);
if (!url) return null;
// 3. Update cache for future requests
await redisClient.set(shortCode, url.originalUrl);
await urlRepository.incrementClicks(shortCode);
return url.originalUrl;
}Benefits:
- Reduces database load
- Improves response time
- Scalable for high traffic
nodejs-lld-url-shortener/
βββ src/
β βββ app.js # Express app setup
β βββ server.js # Server entry point
β βββ config/
β β βββ mongo.js # MongoDB connection
β β βββ redis.js # Redis connection
β βββ controllers/
β β βββ url.controller.js # HTTP request handlers
β βββ services/
β β βββ url.service.js # Business logic layer
β βββ repositories/
β β βββ url.repository.js # Data access layer
β βββ models/
β β βββ url.model.js # Mongoose schema
β βββ strategies/
β β βββ shortCode.strategy.js # Base strategy (interface)
β β βββ base62.strategy.js # Concrete strategy
β βββ routes/
β βββ url.routes.js # API routes
βββ .env # Environment variables
βββ package.json
βββ README.md
- Node.js (v14 or higher)
- Either:
- Docker and Docker Compose (recommended)
- OR MongoDB (running on default port 27017) + Redis (running on default port 6379)
-
Clone the repository:
git clone <repository-url> cd nodejs-lld-url-shortener
-
Install dependencies:
npm install
-
Configure environment variables: Create a
.envfile in the root directory:MONGO_URI=mongodb://127.0.0.1:27017/url_shortener REDIS_URL=redis://127.0.0.1:6379
-
Start MongoDB and Redis:
Option A: Using Docker (Recommended):
# Start MongoDB and Redis containers docker-compose up -d # Stop containers docker-compose down # Stop and remove data volumes docker-compose down -v
Option B: Local Installation:
# Windows - MongoDB mongod # Windows - Redis redis-server # macOS/Linux - MongoDB sudo systemctl start mongod # macOS/Linux - Redis sudo systemctl start redis
-
Run the application:
npm run dev
Server will start at
http://localhost:3000
POST /shorten
Request Body:
{
"originalUrl": "https://www.example.com/very/long/url"
}Response:
{
"shortUrl": "http://localhost:3000/ADsZo3D"
}cURL Example:
curl -X POST http://localhost:3000/shorten \
-H "Content-Type: application/json" \
-d '{"originalUrl":"https://google.com"}'GET /:code
Example:
GET http://localhost:3000/ADsZo3D
β Redirects to https://www.example.com/very/long/url
β Increments click count
Browser/cURL:
curl -L http://localhost:3000/ADsZo3D| Technology | Purpose |
|---|---|
| Node.js | Runtime environment |
| Express.js | Web framework |
| MongoDB | Primary database (persistence) |
| Mongoose | MongoDB ODM |
| Redis | Caching layer |
| nanoid | Short code generation |
| dotenv | Environment configuration |
-
Single Responsibility Principle (SRP)
- Each class has one reason to change
- Controller β handles HTTP
- Service β business logic
- Repository β data access
-
Open/Closed Principle (OCP)
- Strategy pattern allows adding new strategies without modifying service
-
Liskov Substitution Principle (LSP)
- Any
ShortCodeStrategyimplementation can replace another
- Any
-
Interface Segregation Principle (ISP)
- Minimal, focused interfaces (ShortCodeStrategy has only
generate())
- Minimal, focused interfaces (ShortCodeStrategy has only
-
Dependency Inversion Principle (DIP)
- High-level modules depend on abstractions (strategies), not concrete implementations
-
Create new strategy file:
// src/strategies/md5.strategy.js const crypto = require('crypto'); const ShortCodeStrategy = require('./shortCode.strategy'); class MD5Strategy extends ShortCodeStrategy { generate() { const hash = crypto.createHash('md5') .update(Date.now().toString()) .digest('hex'); return hash.substring(0, 7); } } module.exports = MD5Strategy;
-
Use in routes:
const MD5Strategy = require('../strategies/md5.strategy'); const service = new UrlService(new MD5Strategy());
Extend the repository with new methods:
// In url.repository.js
async getAnalytics(code) {
return Url.findOne({ shortCode: code })
.select('originalUrl clickCount createdAt');
}Add corresponding service and controller methods.
{
originalUrl: String (required),
shortCode: String (unique),
expiresAt: Date (optional),
clickCount: Number (default: 0),
createdAt: Date (auto),
updatedAt: Date (auto)
}- Redis caching reduces database load
- Stateless services enable horizontal scaling
- Repository pattern allows database sharding
- Clear separation of concerns
- Each component is independently testable
- New features can be added without breaking existing code
- Dependency injection enables easy mocking
- Each layer can be unit tested in isolation
- Strategy pattern allows new algorithms
- Repository pattern allows database switching
- Layered architecture supports feature additions
MIT License - Feel free to use this for learning and interviews!