URL Shortener system
Below is a comprehensive Low-Level Design (LLD) for a URL Shortener system implemented in Java. This design models the core entities and uses multiple design patterns to achieve a modular, scalable, and maintainable solution. In this design we use:
Singleton Pattern: To ensure that key services (like the URLShortenerService and URLRepository) have only one instance.
Factory Pattern: To encapsulate the creation of URLMapping objects.
Strategy Pattern: To enable different algorithms for generating short URLs (for example, using Base62 encoding or hashing).
DAO/Repository Pattern: To abstract persistence for URL mappings (here simulated as an in‑memory store).
Observer Pattern (Optional): To notify other components when a new URL mapping is created (for example, for analytics or logging).
1. System Overview
Entities
URLMapping: Represents a mapping between the original URL and its shortened version. It contains attributes like original URL, short URL, creation timestamp, and usage count.
Core Services
URLShortenerService (Singleton): Provides APIs to shorten URLs and retrieve the original URL.
URLRepository (DAO Pattern): Abstracts the persistence of URL mappings (in‑memory for simplicity, can be extended to a database).
ShortUrlGeneratorStrategy (Strategy Pattern): Defines how to generate a unique short URL from the original URL. A default implementation uses Base62 encoding of a sequence or hash.
URLMappingFactory (Factory Pattern): Centralizes the creation of URLMapping objects.
Optional Observers
URLCreationObserver (Observer Pattern): Notifies observers when a new URL mapping is created (for logging, analytics, etc.).
2. Detailed Java Code
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
// --------------------- ENTITY ---------------------
// URLMapping represents the mapping between original and shortened URLs.
class URLMapping {
private String originalUrl;
private String shortUrl;
private LocalDateTime createdAt;
private int hitCount;
public URLMapping(String originalUrl, String shortUrl) {
this.originalUrl = originalUrl;
this.shortUrl = shortUrl;
this.createdAt = LocalDateTime.now();
this.hitCount = 0;
}
public String getOriginalUrl() { return originalUrl; }
public String getShortUrl() { return shortUrl; }
public LocalDateTime getCreatedAt() { return createdAt; }
public int getHitCount() { return hitCount; }
public void incrementHitCount() {
hitCount++;
}
@Override
public String toString() {
return "URLMapping{" +
"originalUrl='" + originalUrl + '\'' +
", shortUrl='" + shortUrl + '\'' +
", createdAt=" + createdAt +
", hitCount=" + hitCount +
'}';
}
}
// --------------------- OBSERVER PATTERN ---------------------
// Observer interface for URL mapping creation events.
interface URLCreationObserver {
void onURLCreated(URLMapping mapping);
}
// A concrete observer that logs the URL creation.
class LoggingURLObserver implements URLCreationObserver {
@Override
public void onURLCreated(URLMapping mapping) {
System.out.println("Observer Log: New URL mapping created: " + mapping);
}
}
// --------------------- REPOSITORY (DAO Pattern) ---------------------
// URLRepository interface abstracts persistence of URLMapping objects.
interface URLRepository {
void save(URLMapping mapping);
URLMapping findByShortUrl(String shortUrl);
URLMapping findByOriginalUrl(String originalUrl);
}
// In-memory implementation of URLRepository.
class InMemoryURLRepository implements URLRepository {
private Map<String, URLMapping> shortUrlToMapping = new HashMap<>();
private Map<String, URLMapping> originalUrlToMapping = new HashMap<>();
@Override
public synchronized void save(URLMapping mapping) {
shortUrlToMapping.put(mapping.getShortUrl(), mapping);
originalUrlToMapping.put(mapping.getOriginalUrl(), mapping);
}
@Override
public synchronized URLMapping findByShortUrl(String shortUrl) {
return shortUrlToMapping.get(shortUrl);
}
@Override
public synchronized URLMapping findByOriginalUrl(String originalUrl) {
return originalUrlToMapping.get(originalUrl);
}
}
// --------------------- STRATEGY PATTERN ---------------------
// Strategy Pattern: Defines a contract for generating short URLs.
interface ShortUrlGeneratorStrategy {
String generateShortUrl(String originalUrl);
}
// A default implementation that uses an atomic counter and Base62 encoding.
class Base62UrlGenerator implements ShortUrlGeneratorStrategy {
// Atomic counter to ensure uniqueness.
private AtomicLong counter = new AtomicLong(100000); // starting number
private final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private final int BASE = ALPHABET.length();
@Override
public String generateShortUrl(String originalUrl) {
long id = counter.getAndIncrement();
return encodeBase62(id);
}
// Encodes a number to a Base62 string.
private String encodeBase62(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
int rem = (int)(num % BASE);
sb.append(ALPHABET.charAt(rem));
num /= BASE;
}
return sb.reverse().toString();
}
}
// --------------------- FACTORY PATTERN ---------------------
// Factory Pattern: URLMappingFactory centralizes creation of URLMapping objects.
class URLMappingFactory {
public static URLMapping createURLMapping(String originalUrl, ShortUrlGeneratorStrategy generator) {
String shortUrl = generator.generateShortUrl(originalUrl);
return new URLMapping(originalUrl, shortUrl);
}
}
// --------------------- SINGLETON: URL SHORTENER SERVICE ---------------------
/*
* Singleton Pattern: URLShortenerService provides the main API for the URL shortener.
* It uses the repository to persist mappings and the generator strategy to create short URLs.
*/
class URLShortenerService {
private static URLShortenerService instance;
private URLRepository repository;
private ShortUrlGeneratorStrategy generatorStrategy;
private List<URLCreationObserver> observers;
// Private constructor to enforce singleton.
private URLShortenerService() {
this.repository = new InMemoryURLRepository();
this.generatorStrategy = new Base62UrlGenerator();
this.observers = new ArrayList<>();
}
public static synchronized URLShortenerService getInstance() {
if (instance == null) {
instance = new URLShortenerService();
}
return instance;
}
// Optionally allow setting a custom strategy.
public void setGeneratorStrategy(ShortUrlGeneratorStrategy strategy) {
this.generatorStrategy = strategy;
}
// Register an observer for URL creation events.
public void registerObserver(URLCreationObserver observer) {
observers.add(observer);
}
// Create a short URL for a given original URL.
public String shortenURL(String originalUrl) {
// Check if mapping already exists.
URLMapping mapping = repository.findByOriginalUrl(originalUrl);
if (mapping != null) {
return mapping.getShortUrl();
}
// Create a new mapping using the factory.
mapping = URLMappingFactory.createURLMapping(originalUrl, generatorStrategy);
repository.save(mapping);
// Notify observers.
notifyObservers(mapping);
return mapping.getShortUrl();
}
// Retrieve the original URL from a short URL.
public String getOriginalURL(String shortUrl) {
URLMapping mapping = repository.findByShortUrl(shortUrl);
if (mapping != null) {
mapping.incrementHitCount();
return mapping.getOriginalUrl();
}
return null;
}
private void notifyObservers(URLMapping mapping) {
for (URLCreationObserver observer : observers) {
observer.onURLCreated(mapping);
}
}
}
// --------------------- MAIN APPLICATION ---------------------
public class URLShortenerApp {
public static void main(String[] args) {
URLShortenerService service = URLShortenerService.getInstance();
// Register an observer (e.g., for logging)
service.registerObserver(new LoggingURLObserver());
// Shorten some URLs.
String originalUrl1 = "https://www.example.com/very/long/url/that/needs/to/be/shortened";
String shortUrl1 = service.shortenURL(originalUrl1);
System.out.println("Short URL for " + originalUrl1 + " is " + shortUrl1);
String originalUrl2 = "https://www.anotherexample.com/some/other/long/path";
String shortUrl2 = service.shortenURL(originalUrl2);
System.out.println("Short URL for " + originalUrl2 + " is " + shortUrl2);
// Retrieve original URL.
String retrievedOriginal = service.getOriginalURL(shortUrl1);
System.out.println("Retrieved original URL for " + shortUrl1 + " is " + retrievedOriginal);
}
}
3. Detailed Explanation and Pros/Cons
Entities and Their Interactions
URLMapping: Encapsulates the relationship between an original URL and its shortened version. Additional attributes like creation time and hit count provide useful metadata.
Repository (DAO Pattern): The
URLRepository
interface and its in‑memory implementation abstract away data storage details, making the system more modular and testable.
Key Design Patterns
Singleton Pattern (URLShortenerService):
Pros:
Centralized management and global access to the URL shortener functionality.
Ensures consistent state across the application.
Cons:
Can introduce global state that is hard to test and debug if misused.
Factory Pattern (URLMappingFactory):
Pros:
Centralizes object creation, enabling flexible changes to instantiation logic without impacting client code.
Cons:
Adds an extra layer of abstraction which may be overkill for simple objects.
Strategy Pattern (ShortUrlGeneratorStrategy):
Pros:
Allows easy substitution of different short URL generation algorithms (e.g., Base62 encoding vs. hash‑based).
Promotes open/closed principle; new strategies can be added without modifying existing code.
Cons:
Increases the number of classes and can complicate the design if too many strategies are introduced.
Observer Pattern (URLCreationObserver):
Pros:
Decouples the core URL creation logic from auxiliary processes (such as logging, analytics, or notifications).
Enables dynamic subscription and notification.
Cons:
Requires careful management to avoid memory leaks (e.g., unregistered observers).
Can lead to unpredictable update order if multiple observers are involved.
Overall Pros and Cons for the URL Shortener LLD
Pros:
Modularity: Using design patterns keeps the code clean, modular, and easier to extend (e.g., switching out the URL generation strategy).
Testability: Abstraction via the repository and factory patterns allows for unit testing each component separately.
Scalability: Although the example uses in‑memory storage, the DAO pattern makes it straightforward to switch to a database-backed implementation for higher scalability.
Cons:
Complexity: Introducing multiple design patterns increases the number of classes and interfaces, which may add complexity for small-scale projects.
Overhead: For very simple URL shortening requirements, this layered approach might be more than what is needed.
4. Conclusion
This detailed LLD for a URL Shortener system leverages several well‑known design patterns—Singleton, Factory, Strategy, DAO, and Observer—to create a robust, flexible, and extensible solution. It clearly separates responsibilities: generating short URLs, managing persistence, and notifying external components of changes. Such a design is well suited for technical interviews, demonstrating a deep understanding of OOP principles and design patterns in Java.
Feel free to ask for any further clarifications or additional enhancements!
Last updated