Clarify the requirementsCore idea
Always start by agreeing the scope with your interviewer. It shows you think before you design.
Functional
Turn a long URL into a short one (https://short.ly/abc123). Redirect a short URL to the original. Support custom aliases (short.ly/my-brand). Expire URLs after a configurable time (default 5 years).
Non-functional
100M new URLs a day and 10 reads per write (1B redirects a day). Redirects under 100ms at p99. 99.99% uptime: redirects must never fail. Once created, a URL must never be lost.
Back-of-the-envelope estimates
Writes: 100M / day ≈ 1,160 writes/sec
Reads: 1B / day ≈ 11,600 reads/sec
Record: ~500 bytes (shortCode, longUrl, userId, createdAt, expiresAt)
Storage: 100M × 365 × 5 × 500 B ≈ 91 TB over 5 yearsAPI designDesign
Keep it simple. Two endpoints cover the core features:
POST /api/shorten
Request: { "long_url": "https://...", "custom_alias": "my-link", "expiry": "2027-01-01" }
Response: { "short_url": "https://short.ly/abc123", "expires_at": "2027-01-01" }
GET /:shortCode
Response: 302 redirect to the original URL| Status | Browser behaviour | Use when |
|---|---|---|
| 301 Moved Permanently | Caches the redirect; later clicks skip your servers | You want the least server load |
| 302 Found | Asks your server every time | You need to count every click (most production systems) |
High-level architectureDesign
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
graph LR
Client["Client"] -->|"POST /api/shorten"| LB["Load balancer"]
Client -->|"GET /:code"| LB
LB --> API["API servers<br/>(stateless)"]
API -->|"check first"| Cache["Redis cache"]
API -->|"read / write"| DB[("DynamoDB")]
API -->|"click event"| Q["Kafka"]
Q --> An[("Analytics DB")]
class Client blue
class LB,API orange
class Cache red
class DB violet
class Q yellow
class An green
classDef orange fill:#ffd8a8,stroke:#1e1e1e,color:#1e1e1e
classDef blue fill:#a5d8ff,stroke:#1e1e1e,color:#1e1e1e
classDef green fill:#b2f2bb,stroke:#1e1e1e,color:#1e1e1e
classDef red fill:#ffc9c9,stroke:#1e1e1e,color:#1e1e1e
classDef violet fill:#d0bfff,stroke:#1e1e1e,color:#1e1e1e
classDef yellow fill:#ffec99,stroke:#1e1e1e,color:#1e1e1e
classDef grey fill:#e9ecef,stroke:#1e1e1e,color:#1e1e1eLoad balancer
Spreads requests across API servers. An L7 balancer allows URL-based routing.
API servers
Stateless: create, validate and redirect. Add more to scale.
Redis cache
Holds hot URLs. Even a 20% hit rate saves ~200M database reads a day.
DynamoDB
Main storage: key-value lookups by shortCode, huge write throughput.
Kafka
Keeps analytics off the redirect path: each redirect emits an event asynchronously.
Request flowsHow it works
Creating a short URL
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
sequenceDiagram
participant C as Client
participant API as API server
participant DB as DynamoDB
participant Cache as Redis
C->>API: POST /api/shorten {long_url}
API->>DB: does long_url already exist?
DB-->>API: existing shortCode, or none
alt already exists
API-->>C: existing short_url
else new URL
API->>API: new ID → base62 shortCode
API->>DB: store {shortCode, longUrl, userId, expiresAt}
API->>Cache: SET shortCode → longUrl
API-->>C: short_url
endRedirecting
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
sequenceDiagram
participant C as Client
participant API as API server
participant Cache as Redis
participant DB as DynamoDB
participant Q as Kafka
C->>API: GET /abc123
API->>Cache: GET abc123
alt cache hit
Cache-->>API: longUrl
else cache miss
API->>DB: query shortCode = abc123
DB-->>API: {longUrl, expiresAt}
API->>Cache: SET abc123 → longUrl
end
API->>Q: click event {shortCode, time, geo}
API-->>C: 302 redirect to longUrlDeep dive: generating the short codePattern
This is the core algorithm. Get a unique integer from a distributed ID generator (see ID generation), then write it in base62: the 62 characters a–z, A–Z and 0–9.
const BASE62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
function generateShortCode(id: number): string {
if (id === 0) return BASE62[0];
let code = "";
while (id > 0) {
code = BASE62[id % 62] + code;
id = Math.floor(id / 62);
}
return code;
}
// 0 → "a"
// 61 → "9"
// 62 → "ba"
// 3,500,000,000 → "dY1N24"| Approach | Length | Collisions | Verdict |
|---|---|---|---|
| Unique ID → base62 | 7 chars = 62⁷ ≈ 3.5 trillion codes | None: each ID maps to one code | ✓ Simple, predictable, reversible |
| MD5 / SHA hash | 32+ chars, must be cut short | Cutting it short causes collisions | ✗ Slower and overkill |
| Random code | Any length | Possible: must check the DB first | ~ Extra latency and complexity |
Deep dive: database schemaDesign
Partition key: shortCode (String)
shortCode String "abc123"
longUrl String "https://very-long-url.com/..."
userId String who created it
createdAt Number epoch ms
expiresAt Number epoch ms (null = never)
clickCount Number denormalised counterDeep dive: cachingScale
Key: shortCode ("abc123")
Value: longUrl ("https://...")
TTL: match the URL's expiry
Eviction: LRU (least recently used)
Max memory: 50 GB (~100M entries at 500 B)ScalingScale
Writes
DynamoDB auto-scaling absorbs spikes (a short link in a viral post). Snowflake IDs or a Redis counter keep ID assignment off the database.
Reads
A Redis cluster splits the keyspace across nodes. DynamoDB DAX adds an in-memory layer for microsecond reads.
Geography
API servers and Redis in several regions, DNS routing (Route 53) to the nearest one, and DynamoDB global tables shared by all.
Common interview mistakesPitfalls
Decisions at a glanceSummary
| Decision | Option A | Option B | Recommendation |
|---|---|---|---|
| Redirect type | 301 (cached by browser) | 302 (every hit reaches you) | 302 for analytics, 301 for performance |
| ID generation | Unique ID + base62 | Random + collision check | Unique ID + base62 |
| Database | DynamoDB (NoSQL) | PostgreSQL (SQL) | DynamoDB for scale |
| Analytics | Synchronous | Async via Kafka | Async via Kafka |
| Expiry | DynamoDB TTL | Cron cleanup | DynamoDB TTL |
Frequently asked questionsFAQ
How do you generate a unique short URL?
Generate a unique 64-bit ID, then encode it as base62 to produce a compact 7-character slug. The base62 encoding turns a large numeric space into short strings, and collisions are impossible as long as the underlying ID generator, such as a snowflake ID, is collision-free.
How do you handle redirect performance at scale?
Store the slug-to-long-URL mapping in a database with the slug as primary key, and front it with a read-heavy cache like Redis to absorb the hottest reads. Add load balancers and geolocated DNS so redirects answer in milliseconds for users around the world.
What happens if a custom short URL is already taken?
Check for the slug at creation time. For custom URLs, return a clear error so the user can choose another. For generated slugs, collisions are practically impossible with 62^7 combinations, and a database retry on the rare conflict is enough.

Now explain url shortener to me out loud
I'll give you a design problem and push on the trade-offs, like a real interviewer.

