Users can submit a long URL and receive a unique short URL.
Accessing a short URL redirects the user to the original URL.
URLs are permanent by default, with optional expiration/TTL.
Anonymous URL creation is supported.
Custom aliases are supported as an optional feature.
Analytics are out of scope for the initial version.
Scale: ~100M new URLs/month and ~4K redirects/sec average, ~12K/sec peak.
Read-heavy: The system should efficiently handle a 100:1+ read/write ratio.
Low latency: Redirects should be served with very low latency.
High availability: Redirects should remain available even if a region or server fails.
Global: The service should support users worldwide with multi-region reads.
Durability: URL mappings should not be lost.
Consistency: A short URL must resolve to the correct long URL; writes should avoid conflicting mappings across regions.
We'll assume URL creation remains roughly flat at 100M/month for the initial design. At 1 KB per URL record, this is ~1.2 TB/year or ~6 TB over 5 years. Peak redirect traffic is ~12K requests/sec, resulting in roughly 12 MB/sec of outbound data assuming a 1 KB record. Storage is therefore manageable; read throughput, latency, and availability are the primary design concerns.
POST /v1/urls
Content-Type: application/json
{
"url": "https://example.com/very/long/path",
"expires_at": "2027-09-10T00:00:00Z",
"custom_alias": "my-link"
}
Response:
{
"short_url": "https://short.ly/aB3xY9"
}
expires_at and custom_alias are optional.
GET /{short_code}
Example:
GET /aB3xY9
Response:
302 Found
Location: https://example.com/very/long/path
Since anonymous users don't have accounts, deletion creates an interesting authorization problem. I'd leave DELETE out of v1 unless Codemia asks for URL management.
I'd aim for this:
┌───────────────┐
│ Clients │
└───────┬───────┘
│
▼
┌───────────────┐
│ Load Balancer │
└───────┬───────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ URL Service │ │ URL Service │
│ Server │ │ Server │
└──────┬───────┘ └──────┬───────┘
│ │
└───────────┬───────────┘
│
▼
┌───────────────┐
│ Cache │
│ (short → URL) │
└───────┬───────┘
│ cache miss
▼
┌───────────────┐
│ Database │
│ short → long │
│ URL │
└───────────────┘
But because the system is global, I'd extend that conceptually:
Global Users
│
┌────▼────┐
│ DNS │
│ / Global│
│ LB │
└────┬────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
Region A Region B Region C
│ │ │
┌──▼───┐ ┌─▼────┐ ┌─▼────┐
│Cache │ │Cache │ │Cache │
└──┬───┘ └──┬───┘ └──┬───┘
│ │ │
└────────────┼────────────┘
▼
URL Database
Load balancer / global routing
URL service
POST /v1/urls: generates a short code and stores the mapping.GET /{short_code}: looks up the mapping and returns a redirect.Cache
short_code → long_url.Database
short_code (PK)
long_url
created_at
expires_at
Create URL:
Client
↓
URL Service
↓
Generate unique short code
↓
Database
↓
Return short URL
Redirect:
Client
↓
URL Service
↓
Cache ──hit──→ 302 Redirect
│
miss
↓
Database
↓
Populate Cache
↓
302 Redirect
Use a centralized/distributed Key Generation Service (KGS) that allocates a unique numeric ID for each URL. We then Base62-encode that ID to produce the short code. Because the KGS guarantees each ID is allocated only once, concurrent requests cannot generate the same short code.
Yes. For the database section, I'd keep the first pass deliberately simple. The key insight is that the database is optimized around the redirect lookup:
short_code → long_urlYou can put this into Codemia:
URL mapping table
| ColumnTypeNotes | ||
short_code | VARCHAR(10) | Primary key |
long_url | TEXT | Original URL |
created_at | TIMESTAMP | Creation time |
expires_at | TIMESTAMP NULL | Optional expiration |
The primary key is short_code because every redirect starts with the short code.
The primary key automatically gives us an efficient lookup:
SELECT long_url, expires_at
FROM urls
WHERE short_code = ?;
So the redirect path is an O(log N) indexed lookup if it reaches the database.
We don't need additional indexes for the core redirect path.
If we later add URL management or expiration cleanup, we could add secondary indexes such as:
expires_at
but those aren't necessary for the hot redirect path.
At ~6 TB over five years, storage capacity isn't our main reason to shard.
However, because we're globally distributed and expect ~12K peak redirects/sec, we may eventually shard for throughput and availability.
A natural strategy is:
shard = hash(short_code) % N
This distributes URL mappings relatively evenly across shards.
For example:
short_code
│
hash(code)
│
┌───────────┼───────────┐
▼ ▼ ▼
Shard 0 Shard 1 Shard 2
The application can determine the correct shard directly from the short code, so there's no scatter-gather query.
Because our cache absorbs most reads, the database doesn't actually need to handle the full 12K redirects/sec. The path is:
GET /abc123
│
▼
Cache
/
hit miss
│ │
▼ ▼
302 DB
│
▼
Cache
│
▼
302
So I'd not introduce database sharding in v1 just because the number is 12K/sec. I'd design the schema so it can shard later, while initially relying on replication + caching.
Use a Key Generation Service (KGS) that allocates unique IDs, potentially in pre-allocated ranges to different regions.
Request
↓
URL Service
↓
KGS → unique numeric ID
↓
Base62 / obfuscation
↓
short_code
↓
Database
The KGS guarantees that two concurrent requests never receive the same underlying ID.
Don't expose the raw KGS counter.
Instead, transform/obfuscate the unique ID before Base62 encoding it.
For example:
KGS ID: 123456
↓
deterministic permutation
↓
obfuscated ID
↓
Base62
↓
"a8Kx92Q"
The transformation must be:
A practical approach is a keyed permutation / format-preserving transformation over the ID space, followed by Base62 encoding.
This preserves the uniqueness property of KGS while preventing 1 → 2 → 3 → 4 from being exposed directly.I'd explicitly say "Base62 is encoding, not encryption." Base62 alone does not solve enumeration.
Use the short code to deterministically identify the shard:
GET /a8Kx92Q
↓
URL Service
↓
hash(short_code) % N
↓
Shard 7
↓
DB lookup
This gives us:
One short-code lookup → exactly one database shard.
There is no scatter-gather across all shards.
If we use a consistent-hashing scheme, adding/removing shards causes less key movement than a simple modulo scheme.
Putting everything together:
GET /a8Kx92Q
│
▼
Global Router
│
▼
URL Service
│
┌─────┴─────┐
│ Cache │
└─────┬─────┘
hit │ miss
│ │
│ ▼
│ hash(short_code)
│ │
│ ▼
│ DB Shard 7
│ │
│ ▼
│ validate TTL
│ │
│ ▼
│ populate cache
│
▼
HTTP 302
│
▼
Original URL
POST /v1/urls
│
▼
URL Service
│
▼
KGS
│
│ unique ID
▼
Obfuscate ID
│
▼
Base62
│
▼
short_code
│
▼
Database
│
▼
Return short URL