A URL Shortener (such as TinyURL or Bitly) is a vital tool in modern web architecture. It maps long, complex web addresses (which can easily span hundreds of characters) to short, clean aliases (typically under 10 characters). This makes URLs easy to share in emails, social media posts, and messages.
To build a fully functional URL shortener, we must support two core operations:
encode(longUrl): Generates a unique, short alias for a given long URL. It must be idempotent—meaning multiple requests for the same URL should return the same short code.decode(shortUrl): Looks up the original long URL associated with a short alias to facilitate instant redirecting.
To understand this bidirectional mapping system, think of managing a massive archive of academic research papers. These papers have long, verbose titles like: 'An Empirical Analysis of Latency Patterns in Highly Distributed Microservice Architectures under Peak Load'.
Writing out this full title every time you reference the paper is impractical. Instead, the library system assigns each paper a unique catalog number, like 2f4.
To manage this efficiently, you keep two catalog ledgers:
- Catalog A (Code-to-Title): When a visitor brings you code
2f4, you quickly look it up to find the corresponding paper title. - Catalog B (Title-to-Code): When a researcher submits a paper, you check this ledger first. If the title is already listed, you return its existing code (
2f4) rather than creating a duplicate entry.
HashMap structures, and the catalog numbers are represented by Base36 values.
Design Strategy
Our design leverages two principal mechanisms:
- Base36 Encoding: Standard decimal integers use base 10 (characters 0–9). By switching to base 36 (using 0–9 and a–z), we can represent large numbers using much shorter strings. For example, the auto-incremented ID
1,000,000translates to the Base36 stringlfls. This allows us to keep URLs short. - Double HashMaps:
mapstores the short code as the key and the long URL as the value, ensuring instant redirection.reversestores the long URL as the key and the short code as the value, preventing duplicate registrations.
Step-by-Step Execution Walkthrough
Let's trace the execution of the codec with an empty database:
- Encoding a New URL: A request to encode
"https://leetcode.com"arrives. We check thereversemap; since it's a new URL, we convert the current ID (1) to its Base36 representation ("1"). We store the bidirectional mapping and increment the ID to2. The returned short URL ishttp://short.ly/1. - Encoding Another URL: Next, we encode
"https://google.com". The ID (2) becomes"2". We save the mappings, increment the ID to3, and returnhttp://short.ly/2. - Handling Duplicates: We request
encode("https://leetcode.com")again. The system finds the URL in thereversemap and returns the existing short linkhttp://short.ly/1immediately without incrementing the counter. - Decoding: A request to decode
http://short.ly/1arrives. We strip the domain prefix to get"1", lookup"1"in our decode map, and retrieve the original URL:https://leetcode.com.
Key Code Explanations
Here is why the main logic in the solution is important:
Integer.toString(id++, 36): The core code generation line. It converts the current numeric counter into a Base36 alphanumeric string and increments the counter in a single, atomic operation.if (reverse.containsKey(longUrl)): Deduplication check. Ensures we do not consume database or memory space for repeated queries of the same URL.shortUrl.replace(base, ""): Isolates the base36 code from the domain prefix, allowing us to perform index lookups in our internal map directly.
Java Implementation Code
Below is the complete, self-contained Java source code that solves this problem. It also includes a main method that traces the execution with console outputs.
package io.practise.dsa;
import java.util.*;
public class DesignURLShortener {
public static class Codec {
// Map to decode: stores shortCode -> longUrl
private final Map<String, String> map = new HashMap<>();
// Map to encode/deduplicate: stores longUrl -> shortCode
private final Map<String, String> reverse = new HashMap<>();
private final String base = "http://short.ly/";
private int id = 1;
public Codec() {
// Constructor
}
// Encodes a URL to a shortened URL.
public String encode(String longUrl) {
// Return existing short URL if already processed
if (reverse.containsKey(longUrl)) {
return base + reverse.get(longUrl);
}
// Convert current incrementing ID to a Base36 alphanumeric code
String code = Integer.toString(id++, 36);
// Store mappings bidirectionally
map.put(code, longUrl);
reverse.put(longUrl, code);
return base + code;
}
// Decodes a shortened URL to its original URL.
public String decode(String shortUrl) {
// Strip the base URL prefix to isolate the code
String code = shortUrl.replace(base, "");
return map.getOrDefault(code, "");
}
}
public static void main(String[] args) {
Codec codec = new Codec();
System.out.println("--- Design URL Shortener Demonstration ---");
String originalUrl = "https://leetcode.com/problems/design-tinyurl";
System.out.println("Original URL: " + originalUrl);
String shortUrl = codec.encode(originalUrl);
System.out.println("Encoded (Shortened) URL: " + shortUrl);
String decodedUrl = codec.decode(shortUrl);
System.out.println("Decoded (Restored) URL: " + decodedUrl);
System.out.println("\nChecking deduplication...");
String shortUrlDuplicate = codec.encode(originalUrl);
System.out.println("Second Encoding of same URL: " + shortUrlDuplicate);
System.out.println("Is same short URL generated: " + shortUrl.equals(shortUrlDuplicate));
}
}
Conclusion & Practical Takeaways
This in-memory URL shortener provides high performance with O(1) runtime complexity for both encoding and decoding. By using bidirectional HashMaps and Base36 conversion, we ensure data consistency and minimal memory footprint, forming a strong foundation for a production-grade system.