📌The Interview Question
"Design a URL shortening service like TinyURL or bit.ly"
This is one of the most common system design questions asked at Google, Amazon, Microsoft, and top startups. Here's exactly how to tackle it step-by-step.
📌Why This Question is Asked
Interviewers love this problem because it tests:
📌Step 1: Clarify Requirements (5 mins)
Always ask these questions before jumping to design:
Functional Requirements
Non-Functional Requirements
Back-of-envelope Estimation
Let's estimate the scale:
Interview Tip: Always show your math. It demonstrates structured thinking.
📌Step 2: API Design
// Create short URLPOST /api/v1/shortenRequest:{ "long_url": "https://example.com/very-long-path/with/many/segments", "custom_alias": "my-link", // optional "expiry_date": "2025-12-31" // optional}Response: { "short_url": "https://tiny.url/abc123", "expires_at": "2025-12-31T00:00:00Z", "created_at": "2025-01-12T10:30:00Z"}// Redirect (most important API)GET /{short_code}Response: HTTP 301/302 Redirect to original URLHeaders: Location: https://example.com/very-long-path// Get AnalyticsGET /api/v1/stats/{short_code}Response:{ "total_clicks": 15000, "unique_visitors": 8500, "clicks_by_country": {"IN": 5000, "US": 3000, ...}, "clicks_by_day": [{"date": "2025-01-01", "count": 500}, ...]}// Delete URL (for user-owned URLs)DELETE /api/v1/url/{short_code}📌Step 3: Database Schema Design
Primary URL Table
CREATE TABLE urls ( id BIGINT PRIMARY KEY AUTO_INCREMENT, short_code VARCHAR(7) UNIQUE NOT NULL, long_url TEXT NOT NULL, user_id BIGINT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NULL, click_count BIGINT DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, INDEX idx_short_code (short_code), INDEX idx_user_id (user_id), INDEX idx_expires (expires_at));-- For analytics (separate table for performance)CREATE TABLE url_analytics ( id BIGINT PRIMARY KEY AUTO_INCREMENT, short_code VARCHAR(7), clicked_at TIMESTAMP, user_agent TEXT, ip_address VARCHAR(45), country_code VARCHAR(2), referrer TEXT, INDEX idx_short_code_time (short_code, clicked_at));Database Choice
📌Step 4: Short Code Generation
This is the core algorithm. You have several options:
Option 1: Base62 Encoding (Recommended)
Characters: a-z (26) + A-Z (26) + 0-9 (10) = 62 characters
public class Base62Encoder { private static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; public String encode(long id) { StringBuilder sb = new StringBuilder(); while (id > 0) { sb.append(ALPHABET.charAt((int)(id % 62))); id /= 62; } // Pad to minimum length while (sb.length() < 6) { sb.append('0'); } return sb.reverse().toString(); } public long decode(String shortCode) { long id = 0; for (char c : shortCode.toCharArray()) { id = id * 62 + ALPHABET.indexOf(c); } return id; }}Option 2: Pre-generated Key Service
For high-scale systems:
Option 3: MD5/SHA Hash
public String generateShortCode(String longUrl) { String hash = DigestUtils.md5Hex(longUrl); return hash.substring(0, 7);}Problem: Collisions. Solution: Append random suffix and retry.
📌Step 5: System Architecture
Component Details
📌Step 6: Read Path (Critical for Performance)
The redirect flow handles 90% of traffic:
public class RedirectService { @Autowired private RedisTemplate<String, String> redis; @Autowired private UrlRepository urlRepo; @Autowired private AnalyticsPublisher analytics; public String getLongUrl(String shortCode) { // Step 1: Check Redis cache String longUrl = redis.opsForValue().get("url:" + shortCode); if (longUrl != null) { // Cache hit - publish analytics async analytics.publishAsync(shortCode, request); return longUrl; } // Step 2: Cache miss - query database Url url = urlRepo.findByShortCode(shortCode); if (url == null || !url.isActive()) { throw new UrlNotFoundException(); } // Step 3: Check expiration if (url.getExpiresAt() != null && url.getExpiresAt().isBefore(Instant.now())) { throw new UrlExpiredException(); } // Step 4: Update cache redis.opsForValue().set( "url:" + shortCode, url.getLongUrl(), Duration.ofHours(24) ); // Step 5: Publish analytics asynchronously analytics.publishAsync(shortCode, request); return url.getLongUrl(); }}📌Step 7: Scaling Strategies
Database Sharding
By short_code hash (recommended):
public int getShard(String shortCode) { return Math.abs(shortCode.hashCode()) % NUM_SHARDS;}Cache Warming
CDN for Global Reach
📌Common Interview Questions
Q: Why Base62 instead of Base64? A: Base64 includes + and / which are not URL-safe. Base62 uses only alphanumeric characters.
Q: 301 vs 302 redirect?
Q: How do you prevent abuse?
Q: How do you handle hash collisions?
Q: What happens if Redis goes down?
📌Key Takeaways
This problem tests your ability to design practical, scalable systems. Practice explaining each component's purpose and trade-offs.