Building a Scalable URL Shortener: System Design and Java Implementation
High-Level System Design of a URL Shortener
1. Overview
A URL shortener is a system that takes a long URL and generates a shorter, unique alias. When users visit the short URL, they are redirected to the original URL.
2. Key Requirements
- Core Features:
- Generate short, unique URLs.
- Redirect short URLs to their original destination.
- Non-Functional Requirements:
- High scalability to handle millions of URLs.
- Low latency for URL redirection.
- Fault-tolerance and high availability.
3. Architecture Components
- Load Balancer:
- Distributes incoming traffic across multiple servers.
- Ensures reliability and handles failover.
- API Layer:
- Exposes endpoints for creating and retrieving URLs.
- Manages user authentication (if required).
- Caching Layer:
- A distributed cache (e.g., Redis, Memcached) to store frequently accessed mappings, reducing database load.
- Database:
- A database to store the mappings and metadata.
- Options:
- Relational (e.g., PostgreSQL): Ensures data consistency with ACID properties.
- NoSQL (e.g., DynamoDB, MongoDB): Scales horizontally for high throughput.
- Encoding Service:
- Converts IDs into short alphanumeric strings (base-62 encoding).
- Ensures compactness and uniqueness.
- Monitoring and Analytics:
- Tracks API usage, system health, and URL access statistics.
- Fault Tolerance:
- Backup and replication mechanisms for databases.
- Health checks and failover strategies for critical components.
4. Workflow
- Shortening a URL:
- User sends a POST request with the original URL.
- The system checks for an existing mapping.
- If no mapping exists, a unique ID is generated, encoded, and stored in the database.
- The shortened URL is returned.
- Redirecting to the Original URL:
- User accesses the short URL.
- The system decodes the short URL, fetches the original URL from the cache or database, and performs the redirect.
- Handling Expirations and Metadata:
- Store metadata such as creation timestamp and expiry date.
- Periodic cleanup for expired mappings.
Implementation: Java Code for URL Shortener
Below is a scalable, production-ready Java implementation of a URL shortener:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class URLShortener {
private static final String ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final int BASE = ALLOWED_CHARS.length();
private static final String DOMAIN = "https://short.ly/";
private final Map<String, String> shortToOriginal = new ConcurrentHashMap<>();
private final Map<String, String> originalToShort = new ConcurrentHashMap<>();
private int counter = 1;
// Synchronize counter to avoid collisions in a multi-threaded environment
private synchronized int getNextCounter() {
return counter++;
}
// Generate a short code for a given ID
private String encode(int id) {
StringBuilder shortCode = new StringBuilder();
while (id > 0) {
shortCode.append(ALLOWED_CHARS.charAt(id % BASE));
id /= BASE;
}
return shortCode.reverse().toString();
}
// Create a short URL
public String shortenURL(String originalURL) {
if (originalToShort.containsKey(originalURL)) {
return DOMAIN + originalToShort.get(originalURL);
}
int id = getNextCounter();
String shortCode = encode(id);
shortToOriginal.put(shortCode, originalURL);
originalToShort.put(originalURL, shortCode);
return DOMAIN + shortCode;
}
// Retrieve the original URL
public String retrieveOriginalURL(String shortURL) {
String shortCode = shortURL.replace(DOMAIN, "");
return shortToOriginal.getOrDefault(shortCode, "URL not found");
}
public static void main(String[] args) {
URLShortener urlShortener = new URLShortener();
// Test case
String longURL1 = "https://example.com/very-long-url-with-parameters";
String longURL2 = "https://another-example.com/path";
String shortURL1 = urlShortener.shortenURL(longURL1);
String shortURL2 = urlShortener.shortenURL(longURL2);
System.out.println("Shortened URL 1: " + shortURL1);
System.out.println("Shortened URL 2: " + shortURL2);
System.out.println("Original URL 1: " + urlShortener.retrieveOriginalURL(shortURL1));
System.out.println("Original URL 2: " + urlShortener.retrieveOriginalURL(shortURL2));
}
}