System Design: URL Shortening Service like Bitly
1. Goal
Design a URL shortening service like Bitly.
Users can submit a long URL and receive a short URL. When someone visits the short URL, the system redirects them to the original long URL.
Example:
Long URL: https://example.com/articles/system-design-url-shortener
Short URL: https://sho.rt/abc123
2. Functional Requirements
Core
- Create a short URL from a long URL.
- Redirect users from a short URL to the original long URL.
- Ensure each short URL is short and unique.
Optional
- Custom alias, for example
https://sho.rt/drake-cv. - Expiration time.
- Analytics: click count, referrer, device, country.
- User ownership and link management.
3. Non-Functional Requirements
Important requirements:
- Fast redirects.
- High availability.
- Read-heavy scalability.
- Durable mapping from short code to long URL.
- Unique short code generation.
- Abuse prevention and rate limiting.
- Observability and analytics.
Useful slogan:
Fast redirect first, analytics later.
4. API Design
Create Short URL
POST /api/v1/urls
Content-Type: application/json
Request:
{
"long_url": "https://example.com/some/very/long/path",
"custom_alias": "my-link",
"expires_at": "2026-12-31T00:00:00Z"
}
Response:
{
"short_url": "https://sho.rt/abc123",
"short_code": "abc123"
}
custom_alias and expires_at are optional.
Redirect
GET /{short_code}
Example:
GET /abc123
Host: sho.rt
Response:
302 Found
Location: https://example.com/some/very/long/path
Use 302 instead of 301 if we want analytics, because 301 may be cached aggressively by browsers or CDNs.
5. Short URL vs Short Code
A short URL is composed of:
short_url = short_domain + short_code
Example:
https://sho.rt/abc123
Where:
short_domain = https://sho.rt
short_code = abc123
The user sees the full short_url.
The system uses short_code as the lookup key.
6. High-Level Architecture
Client
|
| DNS resolves sho.rt to CDN / Load Balancer IP
v
CDN
|
v
API Gateway / Load Balancer
|
|-- POST /api/v1/urls --> Write Service
|
|-- GET /{short_code} --> Read Service
|
|-- Redis Cache
|
|-- Database
Optional analytics pipeline:
Read Service
|
v
Message Queue
|
v
Analytics Workers
|
v
Analytics Database
7. DNS, CDN, and API Gateway
DNS only resolves domain to IP.
Example:
sho.rt -> CDN or Load Balancer IP
DNS does not understand HTTP paths like:
GET /abc123
POST /api/v1/urls
The API Gateway or L7 Load Balancer routes requests based on path and method:
POST /api/v1/urls -> Write Service
GET /{short_code} -> Read Service
For MVP, CDN is mainly used for:
- TLS termination.
- DDoS protection.
- Edge routing.
- Basic rate limiting.
Redis remains the main cache for short_code -> long_url.
CDN can also cache redirect responses for very hot stable links, but this makes expiration, deletion, update, and analytics harder.
8. Create Flow
When a user shortens a URL:
1. Client sends POST /api/v1/urls with long_url.
2. API Gateway routes the request to Write Service.
3. Write Service validates the long URL.
4. Write Service generates a unique short code.
5. Write Service stores short_code -> long_url in the database.
6. Service returns the final short URL.
Example:
long_url = https://example.com/article
short_code = abc123
short_url = https://sho.rt/abc123
9. Redirect Flow
When a user opens a short URL:
1. User clicks https://sho.rt/abc123.
2. DNS resolves sho.rt to CDN or Load Balancer IP.
3. Browser sends GET /abc123.
4. API Gateway routes the request to Read Service.
5. Read Service extracts short_code = abc123.
6. Read Service checks Redis cache.
7. If cache hit, return 302 redirect immediately.
8. If cache miss, query database by short_code.
9. Populate Redis cache.
10. Return 302 redirect.
11. Publish analytics event asynchronously.
If the short code does not exist:
404 Not Found
If the short code exists but has expired:
410 Gone
10. Cache Strategy
Use cache-aside pattern.
GET /abc123
-> check Redis
-> cache hit: return 302
-> cache miss: query DB
-> save result to Redis
-> return 302
Cache format:
key: short_code
value: long_url
Example:
abc123 -> https://example.com/article
For create, cache invalidation is usually not needed because the short code is new.
For update or delete:
1. Update database successfully.
2. Delete cache entry.
3. Future request reloads fresh data from database.
11. Short Code Generation
Use Base62 encoding.
Base62 characters:
0-9, a-z, A-Z
Total: 62 characters.
A 6-character Base62 code gives:
62^6 = 56,800,235,584
That is around 56.8 billion combinations, enough for 1 billion URLs.
Recommended approach:
1. Generate a unique numeric ID.
2. Encode the ID using Base62.
3. Use the encoded string as short_code.
4. Store short_code -> long_url in database.
Example:
id = 125000
base62(id) = aZ3k
short_url = https://sho.rt/aZ3k
Also enforce a unique index on short_code in the database.
12. ID Generator
A global counter can generate unique IDs, but calling it for every create request may become a bottleneck.
Better approach: allocate ID ranges.
Example:
Write Service A gets IDs: 1 -> 1,000,000
Write Service B gets IDs: 1,000,001 -> 2,000,000
Write Service C gets IDs: 2,000,001 -> 3,000,000
Each Write Service generates IDs locally until its range is exhausted.
This avoids calling the ID generator on every request.
13. Database Schema
Table: urls
id
short_code
long_url
user_id
created_at
expires_at
is_active
Important index:
unique index on short_code
Redirect query:
SELECT long_url, expires_at, is_active
FROM urls
WHERE short_code = ?;
14. Repeated Long URL Requests
If a user intentionally shortens the same long URL many times, we can allow multiple short codes.
Reason:
- Different campaigns may need different short links.
- Each short link may have separate analytics.
Example:
sho.rt/facebook-campaign -> same article
sho.rt/email-campaign -> same article
sho.rt/twitter-campaign -> same article
If the client retries because of timeout, use an idempotency key.
POST /api/v1/urls
Idempotency-Key: req-123
If the same request is retried with the same key, return the previous result instead of creating a duplicate.
15. Expiration
When creating a short URL:
expires_at must be null or greater than current_time
If expires_at is in the past:
400 Bad Request
During redirect:
if expires_at <= now:
return 410 Gone
16. Analytics
Do not write analytics synchronously before redirecting.
Bad flow:
GET /abc123
-> write click data to DB
-> return 302
Better flow:
GET /abc123
-> lookup long_url
-> return 302 quickly
-> publish analytics event asynchronously
Analytics pipeline:
Read Service
-> Message Queue
-> Analytics Workers
-> Analytics Database
17. Rate Limiting
Rate limiting can happen at multiple layers.
L4 rate limiting:
- Limits TCP connections.
- Helps against connection floods and basic DDoS.
L7 rate limiting:
- Understands HTTP method, path, headers, API key, and user identity.
- Can limit
POST /api/v1/urlsby user or API key. - Can limit
GET /{short_code}by IP if needed.
Useful phrase:
L4 protects connections.
L7 protects APIs.
18. Scaling to 10k Redirects per Second
To support 10k redirects per second:
- Keep Read Service stateless.
- Scale Read Service horizontally behind a load balancer.
- Use Redis cache for
short_code -> long_url. - Use indexed database lookup on cache miss.
- Process analytics asynchronously.
- Optionally use CDN caching for very hot stable links.
Estimate:
Redirect traffic = 10,000 requests/sec
Redis cache hit rate = 99%
Database reads = 1% of 10,000 = 100 reads/sec
This protects the database from high read traffic.
19. Failure Handling
If Redis fails:
Fallback to database.
System becomes slower but still works.
If database fails:
Cache hits may still work.
Cache misses may return 503.
If analytics queue fails:
Redirect should still work.
Analytics can be dropped or buffered.
If ID Generator fails:
Write Services can continue creating links until local ID ranges are exhausted.
20. Final Summary
The system is read-heavy, so the redirect path must be very fast.
Users create short URLs through POST /api/v1/urls. The Write Service validates the long URL, generates a unique short code using unique ID plus Base62 encoding, stores the mapping in the database, and returns the short URL.
Users are redirected through GET /{short_code}. The Read Service checks Redis first, falls back to the database on cache miss, populates Redis, and returns a 302 redirect.
Analytics should be handled asynchronously through a message queue so that redirect latency stays low.
Final slogan:
Base62 for shortness.
Unique ID for uniqueness.
Redis for fast redirects.
Queue for async analytics.
All rights reserved