+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
Let's register a Viblo Account to get more interesting posts.