My Solution for Designing a Simple URL Shortening Service: A TinyURL Approach

by journey6145

Requirements


Functional Requirements:

  • Generate a short url from long url.
  • Redirect Short Url to Original Url.
  • Support Custom Aliases.
  • Support Link Expiration.


Non-Functional Requirements:

  • High Availability (redirect must always work)
  • Low Latency (<100 ms for redirect)
  • High Scalability (millions of users)
  • Read-Heavy System (redirects >> writes)
  • Horizontal scalability to handle millions of requests by adding more servers instead of scaling vertically
  • Fault tolerance (system should work even if cache or a node fails)


Assumptions

  • 1M Urls created per day.
  • 100M redirects per day.
  • URL Size ~500 bytes


API Design

  • Create Short URL


URL:- POST /shorten

Request:- {

"long_url": "https://example.com/abc",

"custom_alias": "myurl", // optional

"expiry": "2026-12-31" // optional

}

Response:-

{

"short_url" : "https://tinyurl.com/abc123"

}


  • Redirect API

URL: -GET /{short_code}

Response:- HTTP 301 Redirect -> Original URL


  • Analytics API

URL:- GET /analytics/{short_code}





High-Level Design


  • Architecture Flow


Client -> Load Balancer -> API Servers -> Cache -> Database


  • Components

CDN / Edge Layer

  • Cache 301 redirect responses for popular URLs
  • Reduce latency by serving requests closer to users
  • Offload traffic from backend systems


Load Balancer

  • Distributes Traffic across API Servers (Node.js)
  • Performs health checks
  • Ensures high availability


API Gateway

  • Routes requests to appropriate services
  • Handles authentication (if needed)
  • Applies throttling and request validation


Rate Limiter

  • Prevents abuse and traffic spikes
  • Protects backend services from overload


Identifier Generation & Uniqueness


Generate short_code that is:

  • Unique
  • Non-colliding
  • Scalable across multiple servers


Use Distributed ID Generation Service


How it works:

ID = timestamp + machine_id + sequence


Example:

[ 41 bits timestamp ][10 bits machine_id][12 bits sequence]


Why this works:

  • Timestamp → unique over time
  • Machine ID → unique across servers
  • Sequence → handles same-millisecond requests


No collision even at high concurrency


Convert to Short Code

short_code = Base62(ID)


Prevent Guessable IDs


Problem:

Sequential IDs → easy to guess

Solution:

short_code = Base62(ID XOR random_salt)


Collision Handling (Edge Case)

Apply UNIQUE constraint on short_code

If collision → regenerate

To ensure uniqueness of identifiers, I will use a distributed ID generation strategy instead of relying solely on database auto-increment.

The ID will be generated using a combination of timestamp, machine ID, and sequence number (similar to Snowflake), ensuring uniqueness across multiple servers even under high concurrency.

The generated ID will then be encoded using Base62 to create a short URL.

To prevent predictability and URL enumeration, I will introduce randomness by applying a transformation such as XOR with a random salt before encoding.

Additionally, a unique constraint will be enforced at the database level as a safeguard against rare collisions.


Cache (Redis)

  • Stores Frequently Accessed URLs
  • Reduces DB Load


Database (MYSQL/NOSQL)

  • The storage layer is responsible for maintaining persistent URL mappings, ensuring durability, and supporting efficient read/write operations at scale.


  • Request Flow

Redirect Flow

  1. User Clicks on Short URL.
  2. Check Redis
  3. If miss -> query DB
  4. Store in Redis
  5. Redirect User


System is read-heavy. Cache is critical.




Detailed Component Design


URL Generation

Approach - Auto Increment ID -> Base62 encoding

Example - ID = 125 -> "cb"


Problem: Predictable IDs

Sequencial IDs can be guessed.

Solution:

short_code = Base62(ID XOR random_salt)

Prevents enumeration attacks


Scalable Option:

Use Distributed ID Generator

timestamp + machine_id + sequence

Avoids bottleneck of single DB


CONCURRENT CREATES & COLLISION

Auto Increment ensures uniqueness

DB Gurantees atomic inserts


If using random codes

Problem: Collision Possible

Solution: UNIQUE Constraint + retry


REDIRECT Service

Flow:

Check Redis

If hit -> return

If miss -> Db lookup

Cache result

Return 301 Redirect


Why 301?

  • Permanent Redirect
  • Browser + CDN Caching
  • Better Performance


Cache Design (Redis)


Strategy - Cache Aside


Key Design

Key = url:{short_code}

value = long_url


Why Cache

  • Avoid DB Load
  • Reduce Latency


Failure Handling

If Redis Fails

Fallback -> DB


Production Setup

  • Redis Cluster (Sharding)
  • Replication + Failover
  • Eviction Policy (LRU)


DataBase Design (MYSQL)


Schema

CREATE TABLE urls (

id BIGINT PRIMARY KEY,

short_code VARCHAR(10) UNIQUE,

long_url TEXT,

created_at TIMESTAMP,

expiry TIMESTAMP

);


Optimization

  • Index on short_code
  • Read Replicas for Scaling


SCALABILITY


API Layer

Sateless Node.js Servers behind load balancers

  • Horizontal Scalling By adding more instances.
  • No session storage in server (use redis if needed)


Database Scaling

  • Sharding

Partition data using:

shard = hash(short_code) % N;

Each shard handles subset of data.


  • Read Replicas

Writes -> Primary DB

Reads -> Replica DB


Reduces read pressure.


Cache Scaling

  • Redis Cluster

keys distributed across multiple nodes.

Increases memory + throughput


Global Scaling

  • Geo-distributed deployment (multi-region)

Reduces latency for global users.


The system scales horizontally at each layer: stateless API servers, Redis Cluster for distributed caching, and database sharding with read replicas for handling large-scale traffic.



Fault Tolerance


Redis Failure

  • Fallback to DB
  • Increased Latency


Generator Failure

  • Multiple generator instances
  • Retry Mechanism


Split Brain Handling

use machine_id in ID Generation


Traffic Spikes

System should:

  • Rate limit users (Rate Limiting - Redis Based)

use token bucket/ sliding window

Example -

INCR user_ip

EXPIRE 1 Sec

if:

Count > 100 -> reject request


  • Prioritize redirects over writes

Redirects = High Priority

Shorten API = Low Priority


Under load:

Reject POST /shorten

Allow GET redirect


  • Load Shedding

If system overloaded:

Return:

503 Service Unavailable

Retry-After: 5 Seconds


  • Autoscale Servers

Scale API Servers

Scale Redis Cluster

Scale DB Replicas


BURST HANDLING + BACKOFF


Problem:

Clients retry aggressively:

1000 failed requests -> retry instantly -> system collapse


Solution: Exponential Backoff + Jitter


Example:

Retry 1 -> Wait 1 Sec

Retry 2 -> Wait 2 Sec

Retry 3 -> Wait 4 Sec


Add randomness:

Wait = base + 2^n + random(0-100ms)


Why?

Prevents thundering herd problem


Server Hint

Return: 503 + Retry+After header



Analytics (ASYNC)

Do Not Update DB Per request


Solution

  • Use Redis counters OR
  • Use queue (RabbitMQ)


Flow

Redirect -> publish event -> process async -> batch update DB


Expiry & Data Management


Expiry Strategy

  • Soft Delete (mark expired)
  • Do not reuse short codes


Scaling Data

  • Move expired URLs to archive table
  • Keep active table small














Markdown supported