URL Shortener#
Problem statement (interviewer prompt)
Design a service like bit.ly: shorten a long URL into a 6-8 character code, redirect on access, and report basic click analytics. Support custom aliases, expiry, and 100M new URLs/day with 100:1 read:write - at <100ms redirect latency.
flowchart LR
U([User])
W([Web / API])
ID([ID Generator])
KV[(Key-Value Store<br/>short -> long)]
C[(Cache)]
U -->|POST /shorten| W
W --> ID
W --> KV
U -->|GET /:code| W
W --> C
C -. miss .-> KV
W -- 301/302 --> U
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class U,W client;
class KV,C datastore;
class ID compute;
flowchart TB
subgraph Client[Clients]
BR([Browser])
APP([Mobile / API consumer])
end
subgraph Edge
DNS[DNS<br/>tinyurl.com]
CDN[CDN<br/>redirect-edge cache]
LB[L7 LB]
WAF[WAF / Rate Limit]
end
subgraph App[App Tier]
GW[API Gateway]
WRITE[Shorten Service<br/>POST /shorten]
READ[Redirect Service<br/>GET /:code]
ANL[Analytics Service]
end
subgraph ID[ID Generation]
SNOW[Snowflake / Counter<br/>chunked from ZK/etcd]
B62[Base62 encode<br/>7 chars = 62^7 ≈ 3.5T]
CUST[Custom alias check<br/>collision check]
end
subgraph Data[Data Layer]
RDS[(Redis hot cache<br/>code -> url<br/>LRU + TTL)]
BF[Bloom filter<br/>existing codes]
KV[(Sharded KV<br/>Cassandra / DynamoDB<br/>by hash code)]
META[(Postgres<br/>users, custom domains, plans)]
CLICKS[(Click events<br/>Kafka -> ClickHouse)]
end
subgraph Async
Q[[Kafka click topic]]
AGG[[Stream aggregator<br/>Flink / Spark]]
DASH[Dashboard]
end
subgraph Obs[Observability]
M[Metrics: redirect QPS,<br/>p99 latency]
L[Logs]
T[Traces]
end
BR --> DNS --> CDN --> LB --> WAF --> GW
APP --> DNS
GW --> WRITE
GW --> READ
WRITE --> CUST
WRITE --> SNOW --> B62
B62 --> KV
WRITE --> META
READ --> RDS
RDS -. miss .-> KV
KV -. fill .-> RDS
READ --> BF
READ -.fire&forget.-> Q
Q --> AGG --> CLICKS --> DASH
App -.metrics.-> M
App -.logs.-> L
App -.traces.-> T
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class BR,APP client;
class DNS,CDN,LB,WAF,GW edge;
class WRITE,READ,ANL,B62,CUST,BF service;
class SNOW,KV,META,CLICKS datastore;
class RDS cache;
class Q,AGG queue;
class DASH,M,L,T obs;
Key design choices#
- ID strategy: pre-generate counter blocks from ZooKeeper/etcd, encode base62, 6-8 chars.
- Alternative: hash(longUrl) → base62 → check collision (deterministic, dedupes).
- Storage: Cassandra/DynamoDB keyed by code, 2 replicas across AZs.
- Redirect: 301 (cacheable, browser caches forever) vs 302 (every hit logged).
- Analytics: never block redirect on logging - async to Kafka.
API#
POST /shorten { url, custom_alias?, ttl? } -> { code, short_url }
GET /:code -> 301/302 to long url
GET /api/v1/:code/stats -> { clicks, geo, referers, time series }
Schema#
url(code PK, long_url, owner_id, created_at, expires_at, custom bool)
clicks(code, ts, ip_hash, ua, geo, referer) -- ClickHouse / BQ
users(id, plan, api_key_hash)
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Load balancer / GSLB | L4/L7 traffic distribution and failover | load-balancer |
HLD |
CDN | edge caching for static assets | cdn |
HLD |
API gateway / BFF | single ingress, auth, rate limit, routing | api-gateway |
HLD |
Pub/Sub & message brokers | topics, consumer groups, delivery semantics | pub-sub-pattern |
HLD |
Leader/follower replication | sync/semi-sync/async replication, failover | replication-leader-follower |
HLD |
Probabilistic data structures | Bloom, HLL, Count-Min, MinHash, t-digest | probabilistic-data-structures |
HLD |
Observability | metrics, logs, traces, SLOs | observability |
HLD |
Batch & stream processing | Lambda vs Kappa, watermarks, windows | batch-stream-processing |
LLD |
Behavioural patterns | Strategy, Observer, State, Command, Chain | behavioral-patterns |
Quick reference#
Functional#
- Shorten arbitrary URL → ≤ 7-char code.
- Redirect on GET.
- Optional custom alias.
- Optional TTL / expiry.
- Click analytics.
Non-functional#
- 99.99% availability on redirect.
- < 100 ms p99 redirect latency.
- Read:Write ≈ 100:1.
Capacity (back-of-envelope)#
- 100M new URLs/day → 1,160 writes/s avg, 5k peak.
- 100:1 reads → 100k reads/s avg, ~500k peak.
- 5 years × 100M × 500 B ≈ 100 TB storage.
- Hot working set ~10 GB cache.
ID design math#
- Base62 with 7 chars: 62^7 ≈ 3.5 × 10^12 codes. Plenty for 100B URLs.
- Pre-allocate ranges of 100k IDs per app instance via ZooKeeper counter (no per-request coord).
API#
POST /v1/shorten body={url, alias?, ttl_days?}
GET /{code} -> 301 redirect
GET /v1/{code} -> { url, created_at, clicks }
DELETE /v1/{code} (owner only)
Data model#
url_map(code PK, long_url, owner, expires_at)- Cassandra / DynamoDB.clicks- ClickHouse (event store).meta- Postgres (users, plans, billing).
Trade-offs#
- 301 vs 302: 301 cached → fewer hits, worse analytics. Most use 302.
- Counter-based code is short & sequential (enumerable); use base62 with random salt or hash-based to avoid scraping.
- Hash-based dedup prevents duplicate shortens of same URL but complicates per-user codes.
- Eventually consistent stats acceptable; redirect must be strongly consistent (or read-your-write within session).
Refs#
- bit.ly engineering blog, TinyURL/Bitly architecture talks, ByteByteGo URL shortener video, Grokking SDI.
FAQ#
How do you generate short codes?#
Either hash the URL and take a base62 prefix with collision retry, or assign a monotonically increasing counter from an ID service and base62 encode the integer.
Should the redirect be 301 or 302?#
Use 302 to keep analytics flowing through your service on every click. Use 301 if SEO and browser caching matter more than tracking.
What database fits a URL shortener?#
A key-value store like DynamoDB, Cassandra, or Redis backed by an OLTP database fits the 100:1 read ratio, with the short code as the partition key.
How do custom aliases work without collisions?#
Treat the alias namespace as reserved and check the KV store before inserting. Generated codes use a distinct prefix or length class so they never collide with custom ones.
How do you handle 100M new URLs per day?#
Estimate 1200 writes per second average and 10x peak. Shard the KV store by code, front reads with a CDN, and use a 64-bit ID generator on the write path.
Related Topics#
- Consistent Hashing: distributes short-code lookups across database shards with minimal hotspots
- Caching Strategies: read-aside caching of hot short-code mappings is the primary latency optimization
- Database Sharding: horizontal sharding of the mapping table handles billions of short links
Further reading#
Curated, high-credibility sources for going deeper on this topic.
- ✍️ Blog - Bitly Engineering - Building Bitly's link shortener
- ✍️ Blog - Instagram Engineering - Sharding and IDs at Instagram
- ✍️ Blog - ByteByteGo - Design a URL shortener