Build a URL Shortener
Difficulty: Hard · Category: URL Shortener · Languages: Java 17, Python 3.11, Go 1.22, Node.js 20, TypeScript 5.x
URL Shortener
Background
URL shortening is a classic system design problem and a fundamental service used across the internet. Services like bit.ly and tinyurl.com convert long, unwieldy URLs into short, shareable links while tracking click analytics.
Building a URL shortener requires thinking about:
- Unique code generation without collisions
- Persistent storage with fast lookups in both directions
- Concurrency — multiple users shortening simultaneously
- Atomic click counting to avoid lost updates
Your Task
Implement the URLShortenerContract interface. Your implementation must:
- Generate unique short codes of 6–8 alphanumeric characters.
- Be thread-safe: concurrent shortening must never produce duplicate codes.
- Increment click counts atomically on each resolve.
- Persist all data — state must survive process restarts.
- Shortening the same URL multiple times creates separate mappings (each with its own short code and click counter).
Recommended infrastructure:
postgres— relational lookups suit this problem well, but you may use any available system.
How It Works
The platform injects your chosen infrastructure SDK(s) via constructor. For example, if you select PostgreSQL:
public class MyURLShortener implements URLShortenerContract {
private final PostgresClient db;
public MyURLShortener(PostgresClient db) {
this.db = db;
}
// ... implement methods using db.execute(), db.query(), etc.
}
See the SDK Docs tab for the full API reference of your chosen system.
Interface
public interface URLShortenerContract {
String shorten(String longUrl);
String resolve(String shortCode);
boolean delete(String shortCode);
int getClickCount(String shortCode);
Map<String, Object> getStats(String shortCode);
}
Method Details
| Method | Description |
|---|---|
shorten | Stores the URL and returns a unique 6–8 character alphanumeric code. Throws IllegalArgumentException for null input. |
resolve | Returns the original URL and increments the click counter atomically. Returns null if the code does not exist. |
delete | Removes the mapping. Returns true if it existed, false otherwise. |
getClickCount | Returns the number of times resolve was called for this code. Returns 0 for unknown codes. |
getStats | Returns a map with keys: shortCode, longUrl, clickCount, createdAt (epoch millis). Returns an empty map for unknown codes. |
Constraints
longUrlis a non-null string (may be empty — your choice how to handle)- Short codes must be exactly 6–8 alphanumeric characters (a–z, A–Z, 0–9)
- Must handle URLs up to 2,048 characters
- Must handle Unicode URLs correctly
- Performance target: ≥ 5,000 shorten ops/sec with p99 latency ≤ 10 ms
- Must support at least 10,000 concurrent shortened URLs
Example Walkthrough
// 1. Shorten two URLs (same URL → different codes each time)
shorten("https://example.com/long/path") → "aBc12D"
shorten("https://example.com/long/path") → "xYz98Q"
shorten("https://other.com/page") → "Mn3pRs"
// 2. Resolve — returns original URL and increments click count
resolve("aBc12D") → "https://example.com/long/path"
resolve("aBc12D") → "https://example.com/long/path"
getClickCount("aBc12D") → 2
getClickCount("xYz98Q") → 0 (never resolved)
// 3. Stats — full details for a code
getStats("aBc12D") → { "shortCode": "aBc12D",
"longUrl": "https://example.com/long/path",
"clickCount": 2,
"createdAt": 1712582400000 }
getStats("unknown") → {} (empty map)
// 4. Delete — removes mapping, future resolves return null
delete("aBc12D") → true
resolve("aBc12D") → null
getClickCount("aBc12D") → 0
delete("aBc12D") → false (already deleted)
Hints
- Base62 encoding (a-z, A-Z, 0-9) gives 62⁶ ≈ 56 billion unique 6-char codes — more than enough. Use a counter or random approach.
- For collision handling, consider:
- A sequence/counter converted to Base62 (no collisions by design)
- Random generation with retry on uniqueness violation
- Hash-based approach with truncation + collision detection
- Ensure click count increments are atomic — avoid read-modify-write race conditions.
- Design your storage so that lookups by short code are fast (indexed or keyed directly).
Infrastructure your code runs against
- PostgreSQL 16
How your solution is graded
Your score combines functional correctness (40%), performance (40%) and code quality (20%).
- Target throughput: 5,000 operations/second
- Target p99 latency: 10 ms
- Load test concurrency: 8 concurrent clients
- Load test duration: 10 seconds
- Maximum error rate: 0.1%