Core features: users can submit a long URL and get a short URL back. Users can access the short URL and get redirected to the original. Users can optionally set a custom alias. Users can set expiration dates on links. Analytics are tracked per click - timestamp, geographic location, referrer, device type.
Availability over consistency - if we have to choose, we'd rather serve a slightly stale URL than return an error. Target 99.99% uptime for the redirect service.
Latency - redirect response should be under 50ms at P99. URL creation can be slightly slower, maybe under 200ms.
Scale - support 100 million URLs created per day, and about 10 billion redirects per day. That's roughly 1,000 writes per second and 115,000 reads per second.
Durability - once a URL is created, it should never be lost. Analytics can tolerate some loss.
For URL creation:
POST /api/v1/urls
Body: { long_url, custom_alias?, expiry_date? }
Response: { short_url, long_url, created_at, expiry_date }
For redirection:
GET /{short_code}
Response: 301 or 302 redirect to long_url
I'd use 302 temporary redirect instead of 301 because 301 gets cached by browsers, which would break our click analytics.
For analytics:
GET /api/v1/urls/{short_code}/analytics
Response: { total_clicks, clicks_by_country, clicks_by_device, clicks_over_time }
Describe the overall system architecture. Identify the main components needThe core flows are two things: URL shortening and URL redirection. Shortening happens maybe a few thousand times per second. Redirection is the hot path - potentially hundreds of thousands of requests per second - so that needs to be blazing fast.
For the short URL generation, I'd use a base62 encoding scheme - characters a-z, A-Z, 0-9. A 7 character code gives us 62^7 which is about 3.5 trillion unique URLs - plenty of headroom.
For the system components, I'd have an API layer behind a load balancer, a URL generation service, a redirect service, an analytics service, and a caching layer.
The redirect service is the most critical - it needs to be fast. So I'd put Redis in front of the database for URL lookups. Most popular URLs get cached, so the majority of redirects never even hit the database.ed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
Let me walk through each component one by one.
URL Generation Service
The core challenge is generating unique short codes at scale without collisions. I'd use two approaches depending on the case.
For auto-generated codes, I'd use a distributed ID generator based on a modified Snowflake ID - this gives me unique IDs across multiple servers without coordination. I then base62 encode that ID to get a 7 character short code. This is fast and collision-free because the ID is globally unique before encoding.
For custom aliases, I check uniqueness in the database before accepting. If the alias already exists, I return a 409 conflict error immediately.
Database Design
I'd use two separate databases for different access patterns.
For URL storage I'd use Cassandra - it's optimized for high write throughput and scales horizontally. The schema would be:
urls table:
- short_code (partition key)
- long_url
- user_id
- created_at
- expiry_date
- is_active
For analytics I'd use a time-series database like Apache Cassandra or ClickHouse - optimized for append-only writes and time-range queries:
clicks table:
- short_code
- timestamp
- country
- device_type
- referrer
- ip_hash
Caching Layer
Redis sits in front of Cassandra for URL lookups. When a redirect request comes in, I check Redis first. Cache hit means instant redirect. Cache miss means I query Cassandra, then populate Redis with a TTL.
I'd use an LRU eviction policy because most traffic follows a power law - a small percentage of URLs get the majority of clicks. I'd size the cache to hold the top 20% of URLs which would handle about 80% of traffic.
For cache invalidation, when a URL expires or gets deactivated, I immediately delete it from Redis so we don't serve stale redirects.
Redirect Service
This is the most performance-critical component. The flow is:
First check Redis - if found and not expired, return 302 redirect immediately. If not in Redis, query Cassandra, populate Redis, then redirect. If URL is expired or not found, return 404.
I'd deploy this as a stateless service behind a load balancer with aggressive horizontal scaling. Each instance can handle thousands of requests per second since most requests are just Redis lookups.
Analytics Pipeline
I wouldn't process analytics synchronously in the redirect flow because that would add latency. Instead, when a redirect happens, I publish a click event to Kafka asynchronously. The redirect returns immediately to the user while Kafka handles the rest.
A separate analytics consumer service reads from Kafka, batches events, and writes to ClickHouse. This gives us eventual consistency for analytics - clicks might show up with a few seconds delay, which is totally acceptable.
For the analytics API, I'd pre-aggregate common queries - like daily click counts per short code - and store those aggregations separately so analytics queries are fast without scanning raw click data.
Expiration Handling
I'd run a background job - maybe every hour - that scans for expired URLs and marks them inactive in Cassandra and deletes them from Redis. I also check expiry at redirect time as a safety net in case the background job hasn't run yet.
Rate Limiting
To prevent abuse, I'd implement rate limiting at the API gateway level using a token bucket algorithm. Anonymous users get maybe 10 URL creations per hour. Authenticated users get higher limits. This protects against URL spam and abuse.