Difficulty: Intermediate
Design a URL shortening service like bit.ly. How would you generate unique short URLs and handle billions of redirects?
Core requirements: 1. Given a long URL, generate a unique short URL 2. Given a short URL, redirect to the original long URL 3. Short URLs should be as short as possible 4. Analytics: track click counts, geographic data
Key design decisions: - Short code generation: Base62 encoding (a-z, A-Z, 0-9) with 7 characters gives 62^7 = 3.5 trillion combinations - Storage: SQL database with caching layer (Redis) - Read-heavy system: 100:1 read-to-write ratio, so optimize for reads - 301 (permanent) vs 302 (temporary) redirect affects caching and analytics
// Approach 1: Counter + Base62 encoding
const BASE62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
function encodeBase62(num) {
let encoded = '';
while (num > 0) {
encoded = BASE62[num % 62] + encoded;
num = Math.floor(num / 62);
}
return encoded.padStart(7, 'a');
}
// Approach 2: MD5 hash + take first 7 chars
function hashApproach(longUrl) {
const hash = md5(longUrl);
return hash.substring(0, 7);
// Handle collisions: append counter and rehash
}
// Approach 3: Pre-generated unique IDs
// Use a separate key generation service (KGS)
// that pre-generates unique 7-char codes
// and stores them in a database
// Database schema
// CREATE TABLE urls (
// id BIGSERIAL PRIMARY KEY,
// short_code VARCHAR(7) UNIQUE NOT NULL,
// long_url TEXT NOT NULL,
// user_id INT,
// created_at TIMESTAMP DEFAULT NOW(),
// expires_at TIMESTAMP,
// click_count BIGINT DEFAULT 0
// );
// CREATE INDEX idx_short_code ON urls(short_code);
Base62 with auto-incrementing counter is simplest. Hash approach avoids sequential IDs but needs collision handling. Pre-generated keys scale best for distributed systems.
// Redirect endpoint
app.get('/:shortCode', async (req, res) => {
const { shortCode } = req.params;
// 1. Check Redis cache first
let longUrl = await redis.get(`url:${shortCode}`);
if (!longUrl) {
// 2. Cache miss - query database
const record = await db.query(
'SELECT long_url FROM urls WHERE short_code = $1',
[shortCode]
);
if (!record) return res.status(404).send('URL not found');
longUrl = record.long_url;
// 3. Populate cache (TTL: 24 hours)
await redis.set(`url:${shortCode}`, longUrl, 'EX', 86400);
}
// 4. Async analytics (don't block redirect)
analyticsQueue.push({
shortCode,
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: Date.now()
});
// 5. 302 redirect (use 301 for permanent)
res.redirect(302, longUrl);
});
// Cache hit ratio target: >90%
// Most popular URLs stay in cache
// LRU eviction for cache management
Redis caching handles the read-heavy load. Analytics are processed asynchronously via a queue so redirects remain fast. Use 302 for tracking clicks, 301 if caching is preferred.
// Analytics are processed asynchronously via a message queue
// to avoid slowing down the redirect response
// Analytics consumer (processes click events from queue)
async function processClickEvent(event) {
const { shortCode, ip, userAgent, timestamp } = event;
// 1. Increment click count (Redis for real-time)
await redis.hincrby(`stats:${shortCode}`, 'clicks', 1);
// 2. Store detailed analytics
await db.analytics.create({
shortCode,
ip,
country: await geoIp.lookup(ip),
browser: parseUserAgent(userAgent).browser,
os: parseUserAgent(userAgent).os,
referrer: event.referrer || 'direct',
timestamp: new Date(timestamp)
});
}
// Analytics API
app.get('/api/analytics/:shortCode', auth, async (req, res) => {
const { shortCode } = req.params;
const [totalClicks, dailyClicks, topCountries, topBrowsers] = await Promise.all([
redis.hget(`stats:${shortCode}`, 'clicks'),
db.analytics.groupBy({
where: { shortCode },
by: ['date'],
_count: true,
orderBy: { date: 'desc' },
take: 30
}),
db.analytics.groupBy({
where: { shortCode },
by: ['country'],
_count: true,
orderBy: { _count: { _all: 'desc' } },
take: 10
}),
db.analytics.groupBy({
where: { shortCode },
by: ['browser'],
_count: true
})
]);
res.json({ totalClicks, dailyClicks, topCountries, topBrowsers });
});
Analytics are decoupled from the redirect flow using a message queue. Click data is aggregated for dashboards showing daily trends, geographic distribution, and browser breakdown.
Hashing, Base62 Encoding, Database Design, Caching, Redirection