+2

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/urls by 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí