How to design a URL shortener like TinyURL or bit.ly in a system design interview: requirements, capacity estimates, key generation, 301 vs 302 redirects, caching, sharding and the follow-ups interviewers ask.
Updated 17 October 2026
A URL shortener maps a long URL to a short key and redirects anyone who visits the short link to the original. Start the interview by agreeing what is in scope - candidates who skip this end up designing features nobody asked for.
A common set of assumptions: 100 million new links a month, a 100:1 read-to-write ratio, links kept for five years, and about 500 bytes per record. The point of the numbers is the conclusion they lead to, so state it.
| Quantity | Calculation | Result |
|---|---|---|
| New links (writes) | 100M ÷ (30 × 86,400 s) | ≈ 40/s average, ~200/s peak |
| Redirects (reads) | 40/s × 100 | ≈ 4,000/s average, ~20,000/s peak |
| Links over 5 years | 100M × 12 × 5 | ≈ 6 billion |
| Storage | 6B × 500 bytes | ≈ 3 TB (≈ 9 TB with 3 replicas) |
| Key length | 62⁷ ≈ 3.5 trillion vs 6B links | 7 characters is ample |
| Cache for hot links | 20% of a day’s ~346M redirects × 500 bytes | Up to ~35 GB - fits a small Redis cluster |
POST /api/v1/urls
{ "long_url": "https://example.com/very/long/path", "custom_alias": "launch", "expires_at": "2027-01-01T00:00:00Z" }
201 Created
{ "short_url": "https://sho.rt/launch", "key": "launch" }GET /{key}
302 Found
Location: https://example.com/very/long/pathurl_mappings
key VARCHAR(16) PRIMARY KEY -- the short key
long_url TEXT NOT NULL
created_at TIMESTAMP
expires_at TIMESTAMP NULL
owner_id BIGINT NULLEvery read is a lookup by key, and there are no joins. That access pattern fits a key-value or wide-column store such as DynamoDB or Cassandra, which shard and replicate natively. A relational database also works at this size if the key is the primary key - choose based on the operational trade-off you can defend, and say why.
If the same long URL should always return the same short link, add a secondary index on a hash of long_url and check it before creating. Many designs skip this deliberately: deduplication costs an extra read on every write, and different users often want separate links for separate analytics.
┌──────────────┐
client ──►│ Load balancer│
└──────┬───────┘
▼
┌───────────────────┐ miss ┌──────────────┐
│ App servers │ ───────► │ Key-value │
│ (stateless) │ ◄─────── │ store │
└──┬─────────┬──────┘ │ (sharded) │
│ hit │ click events └──────────────┘
▼ ▼
┌────────┐ ┌────────┐ ┌───────────────┐
│ Redis │ │ Kafka │──►│ Analytics │
│ cache │ └────────┘ │ aggregation │
└────────┘ └───────────────┘
Key source: leased counter ranges or a key generation serviceThis is the part of the round interviewers probe hardest. There are four common approaches, and the trade-offs between them are the answer.
| Approach | How it works | Strengths | Weaknesses |
|---|---|---|---|
| Hash and truncate | MD5 or SHA-256 of the long URL, Base62-encode, keep the first 7 characters. | No coordination; the same URL gives the same key. | Collisions grow with volume, so every write needs a check-and-retry with a salt. |
| Counter + Base62 | A global counter increments; the number is Base62-encoded. | Never collides; keys stay short. | Sequential keys are guessable, and a single counter is a bottleneck. |
| Leased counter ranges | Each app server leases a block (say 1M IDs) from ZooKeeper or a database, and obfuscates each ID before encoding. | No per-write coordination; unique; not guessable after obfuscation. | A crashed server wastes the rest of its block - harmless with 3.5T keys. |
| Key generation service | A service pre-generates random unused keys; servers fetch batches and mark them used. | Random, unique keys with no collision checks at write time. | Another service to run, and it must never hand out the same key twice. |
Obfuscation means passing the sequential ID through a reversible shuffle before Base62 encoding - for example multiplying by a large number coprime with 62⁷ modulo 62⁷, or a small Feistel network. The mapping stays one-to-one, so uniqueness is preserved, but consecutive IDs no longer produce neighbouring keys.
| 301 Moved Permanently | 302 Found (or 307) | |
|---|---|---|
| Browser caching | Browsers cache it and may skip your server on repeat visits. | Browsers ask your server every time. |
| Analytics | You see the first click per browser, not every click. | Every click reaches you. |
| Changing the destination | Hard - cached copies keep the old target. | Easy - the next visit gets the new target. |
| Server load | Lower. | Higher, which the cache absorbs. |
Pick based on the requirements you agreed: if per-click analytics or editable links are in scope, use 302. If the priority is raw speed and load, 301 is defensible. Either answer is fine when you tie it back to a requirement - choosing without a reason is what loses marks.
Cache key → long URL with an LRU eviction policy and a TTL no longer than the link expiry. Popular links are a small fraction of all links, so a modest cache serves most redirects.
Partition the store by a hash of the short key using consistent hashing, so adding nodes moves only a fraction of keys. Lookups always include the key, so every read hits exactly one shard.
Keep replicas in each region and serve redirects from the nearest one. Short replication lag is acceptable: a link created a second ago is rarely clicked in that second, and the write region can serve misses.
A viral link can concentrate traffic on one cache node. Replicate hot keys across cache nodes, keep a small in-process cache on app servers, or cache the redirect at the CDN edge with a short TTL.
Publish a click event to Kafka and return the redirect immediately. Consumers aggregate counts per link and time bucket into an analytics store, so a slow analytics pipeline never delays a redirect.
Check expires_at on read and return 410, then remove expired rows with a background job. Lazy checks keep reads correct even before the cleanup runs.
| What fails | Impact | Mitigation |
|---|---|---|
| Cache cluster | Every redirect falls through to the store, which may not take 20,000 reads/s. | Replicated cache, read replicas sized for a cold cache, request coalescing for the same key. |
| Key source (ZooKeeper or key service) | New links cannot get keys. | Servers hold a leased range or batch, so they keep creating links until it runs out. |
| A storage shard | Links on that shard fail to resolve. | Replication with automatic failover; serve from replicas. |
| A region | Redirects fail for users routed there. | Multi-region replicas and DNS or anycast failover. |
Core features, custom aliases, expiry, analytics, and what is out of scope.
Writes, reads, storage and key length - then state that the system is read-heavy.
Create and redirect endpoints, status codes, and the key-value record.
Load balancer, stateless servers, cache, store, key source and analytics queue.
Usually key generation, then caching and sharding. Compare approaches and pick one with a reason.
What breaks first, how you would detect it, and what you would build next.
Do not expose sequential keys. Either obfuscate the counter with a reversible shuffle before encoding, or use random keys from a key generation service, and keep the key long enough that random guessing is impractical.
By default they get different keys, which keeps their analytics separate. If deduplication is a requirement, index a hash of the long URL and return the existing key.
Redirects scale horizontally: more stateless servers, more cache nodes and more read replicas. Writes are still only a few thousand a second at 10x, which leased key ranges handle without a central bottleneck.
Update or delete the row and invalidate the cache entry. This is one reason to prefer 302 redirects - a 301 may already be cached in browsers with the old target.
Emit an event to a queue after sending the redirect and aggregate counts in a separate consumer. Approximate real-time counts can come from a counter in Redis, reconciled from the event log.
You can return them to the key pool, but old links may still be printed or bookmarked and would now point somewhere new. With 3.5 trillion keys available, most designs never reuse them.
Yes. It is one of the most common first system design questions because it is small enough to finish in 45 minutes while still testing estimation, key generation, caching, sharding and trade-offs.
Seven Base62 characters give about 3.5 trillion combinations, which is ample for billions of links. Six characters give about 56.8 billion - enough for many systems, but with less headroom.
Yes. "Design TinyURL", "design bit.ly" and "design a URL shortening service" are the same question with different names.
Truncated hashes collide as the dataset grows, so every write needs a collision check and retry. Leased counter ranges with obfuscation, or a key generation service, avoid collisions entirely and are the stronger answer.
Either can work. The access pattern is a simple lookup by key with no joins, which suits key-value stores that shard natively, but a relational database with the key as primary key is fine at this scale. Justify the choice with your estimates.
Use 302 when click analytics or editable destinations are requirements, because browsers ask your server on every visit. Use 301 when speed and lower load matter more, accepting that browsers may cache the redirect.
You can practise the URL shortener question free on InstaMock with an AI interviewer that asks for your requirements and estimates, probes your key generation and caching choices, and gives scored feedback.
Reading a design is not the same as defending one. Book a live 1:1 round with an engineer who has taken hiring decisions: they ask the follow-ups, push on the weak parts, and send written feedback.
Book a 1:1 Mock Interview - ₹499Not ready yet? Practise this question free with the AI interviewer