URL Shortener (TinyURL / bit.ly)
Problem statement
Design a service that takes a long URL and returns a short alias (e.g. sho.rt/aB3xY9). Visiting the alias redirects to the original URL. Aliases may optionally expire and users may request custom aliases.
Requirements
Functional
Create a short URL for a given long URL; optional custom alias and optional expiry.
Redirect a short URL to its long URL with an HTTP 301/302.
Basic analytics: click count per short URL (nice-to-have).
Delete or expire links.
Non-functional
Highly available — redirects must work even if creation is degraded.
Low latency redirect (< 50 ms p99), since it sits in the user's critical path.
Read-heavy: ~100 reads per write.
Short codes must be unpredictable enough to prevent enumeration.
Durable: a link must never resolve to the wrong target.
Back-of-the-envelope estimates
- Writes: 100M new URLs/month → ~40 writes/s (peak ×10 = 400/s).
- Reads: 100:1 → ~4,000 reads/s, peak 40k/s.
- Storage: 100M/month × 5 years = 6B rows. ~500 bytes/row (long URL + metadata) → 3 TB.
- Short-code space: base62, 7 chars → 62^7 ≈ 3.5 trillion, plenty.
- Cache: 20% of daily reads cover 80% of traffic → cache ~20% of hot links: 4k/s × 86,400 × 0.2 × 500 B ≈ 35 GB/day of hot data; a few Redis nodes.
API
POST /v1/urls
body: { longUrl, customAlias?, expiresAt? }
→ 201 { shortUrl, code, expiresAt }
GET /{code}
→ 302 Location: <longUrl> (301 if you want browsers to cache; 302 keeps analytics)
DELETE /v1/urls/{code} (auth required)
GET /v1/urls/{code}/stats → { clicks, createdAt }
Use 302 by default: 301 is cached by browsers, so the redirect stops hitting your servers and you lose click analytics.
Data model
urls
code VARCHAR(8) PK -- base62
long_url TEXT
user_id BIGINT
created_at TIMESTAMP
expires_at TIMESTAMP NULL
clicks BIGINT -- or a separate counter store
Key-value store (DynamoDB / Cassandra) keyed by code. All lookups are by primary key; no joins.
High-level design
- API gateway / load balancer in front of stateless app servers.
- Write path: generate a code, write
code → long_urlto the KV store, return. - Read path: look up
codein Redis; on miss read the KV store, populate cache, 302 redirect. - Code generation — two sound options:
- Hash & truncate: MD5/SHA of the long URL, take first 7 base62 chars, retry on collision. Simple but needs a collision check per write.
- Pre-generated key service (KGS): a service pre-mints unique random codes into a
unused_keystable and hands them out; app servers grab batches of, say, 1,000 keys and keep them in memory. No collision checks, no DB round-trip per write. - Counter + base62 (via a range allocator like ZooKeeper/Flickr ticket server) is also fine but produces guessable sequential codes unless you shuffle/obfuscate.
- Analytics — publish click events to Kafka; aggregate asynchronously. Never write to the primary DB on the read path.
- Expiry — lazy delete on read (return 404 if expired) plus a periodic cleanup job.
Architecture diagram
Deep dives
Why KV instead of relational? Single-key lookups, no relationships, need for horizontal partitioning — a wide-column or KV store partitioned on code scales linearly.
Custom aliases: treat as a conditional put (INSERT IF NOT EXISTS); return 409 on conflict.
Caching strategy: cache-aside with LRU; TTL matched to link expiry. Warm the cache from the previous day's top-N codes after a deploy.
Redirect status: 302 for analytics; 301 if you explicitly want to offload traffic to browser caches.
Bottlenecks & mitigations
Cache hot-spot: a viral link sends all reads to one Redis shard → replicate hot keys or use client-side caching with short TTL.
KGS single point of failure → run KGS as a replicated service; each app server holds a local batch so brief KGS outages don't block writes.
Analytics write amplification → never update
clickssynchronously; batch through Kafka.
Failure modes
- Cache down: reads fall through to the KV store; provision the DB for at least 30% of peak read load so a cache outage degrades latency, not availability.
- KV partition unavailable: use a store with multi-AZ replication (DynamoDB/Cassandra RF=3); reads with
ONEconsistency remain available. - Duplicate code issued (KGS bug): detect with a conditional write; the second writer fails and retries with a new key.
- Malicious/phishing links: run URLs through a safe-browsing check on creation; allow reporting and takedown.
Follow-up questions interviewers ask
How would you support per-user link history and listing?
How would you rate-limit creation to stop abuse?
How would you geo-route redirects (different targets by country)?