Course Content
System Design Interview
31 sections · 71 lessons
URL Shortener: requirements, scale, API and data model
The prompt: "Design a URL shortening service like TinyURL or Bitly."
Attempt it for 45 minutes on paper before reading. This is the most-asked warm-up system in the industry — it looks trivial, and the follow-ups are where it stops being trivial.
This lesson covers the first half of the answer: the questions, the numbers, and the design they produce. Five numbers follow from a handful of assumptions, and each one rules something in or out. They lead to two endpoints and one table, where the interesting decisions are the primary key and the redirect status code. Key generation, the read path at scale and the follow-ups are in the next lesson.
What the system does
Given a long Uniform Resource Locator (URL), return a short one. When someone visits the short URL, redirect them to the original. Two operations, one table, and an enormous read-to-write imbalance that shapes every decision.
The questions
1. How long should the short URL be? This is a real design input, not decoration. The character count decides how many URLs you can ever store, so it must be checked against the five-year volume. Ask what the domain is, too: short.ly/abc1234 is nineteen characters, example-company.com/abc1234 is thirty-four, and if the point is fitting in a message, the domain matters more than the key.
2. Are custom aliases supported? short.ly/summer-sale instead of a generated key. It sounds like a small feature and it changes the design: custom aliases share a namespace with generated keys, so the generator must never produce a key someone has claimed, and you need a reservation and moderation policy.
3. Do links expire? If yes, expiry is a field and cleanup is a job. If no, storage grows forever and you must size for it.
4. Are click analytics required? If yes, every redirect has something else to do, and the choice of redirect status code (at the end of this lesson) becomes consequential — a cached redirect never reaches your servers and therefore never gets counted.
5. What is the read-to-write ratio? Ask this explicitly. The answer is extreme — a link is created once and followed many times — and it is the single number that drives the whole design. Assume 100:1 unless corrected.
Two more worth asking
Can two people shortening the same long URL get the same short one? Deduplicating saves storage and breaks per-user analytics and expiry, since the two users may want different settings. The usual answer is no deduplication, or deduplication only within one user's links.
Is this a public service or internal? A public one needs rate limiting (Section 6), abuse detection, and malicious-URL blocking, all of which are in Follow-ups. An internal link shortener needs almost none of it.
The requirements to state back
- Functional: shorten a long URL to a short key; redirect a short URL to the original; optional custom alias; optional expiry; click counts.
- Non-functional: redirects must be fast, because they sit in a user's navigation — target under 100 ms at the 99th percentile. High availability, because a dead shortener breaks every link ever shared. Short keys must never collide. Shortening can be slower than redirecting; nobody minds waiting 300 ms to create a link.
The redirect path is latency-critical and the creation path is not, which means the two paths get designed separately.
Assumptions, stated out loud
Five numbers come next, computed in the order from The four quantities you always compute.
"I'll assume 100 million new links a day, a 100:1 read-to-write ratio, a five-year retention horizon, and an average long URL of about 100 bytes. Stop me if any of that is wrong."
Write throughput
100 million ÷ 100,000 seconds = 1,000 writes per second average
Peak at 3× = 3,000 writes per second
One thousand writes per second is within reach of a single well-tuned relational leader but leaves little headroom, so the write path needs either a store built for it or a sharded one. Not dramatic, and worth noting so you do not over-build it.
Read throughput
1,000 × 100 = 100,000 reads per second average
Peak at 3× = 300,000 reads per second
This is the number that defines the system. Three hundred thousand reads per second cannot come from a database. They come from memory, and the design becomes "how do we serve almost everything from cache".
Storage
Row contents: short key (7 bytes), long URL (~100 bytes), owner identifier (8 bytes), created timestamp (8 bytes), expiry (8 bytes) ≈ 130 bytes. With index and per-row overhead, call it 500 bytes.
100 million × 500 bytes = 50 GB a day
× 365 = 18 TB a year → 91 TB over five years
× 3 for replication = ~270 TB
Large, but unremarkable for a distributed store. It is decidedly too large for one machine, which settles the sharding question without argument.
How many keys do we need?
100 million/day × 365 × 5 = 182.5 billion keys
Now check the key length. Base 62 uses digits, lowercase, and uppercase — 62 characters:
| Length | Key space |
|---|---|
| 6 | 62^6 ≈ 56.8 billion — not enough |
| 7 | 62^7 ≈ 3.52 trillion — enough, with 19× headroom |
| 8 | 62^8 ≈ 218 trillion |
Seven characters. That derivation — 182.5 billion needed, 6 characters gives 56.8 billion, 7 gives 3.5 trillion — is the answer to "how long should the key be?", and it takes twenty seconds.
Cache size
Reads follow a heavy-tailed distribution: a link is popular for a day or two after it is shared and then almost never used. Assume the standard rule of thumb that 20% of links account for about 80% of reads, applied to the links read in a day.
Distinct links read in a day: call it 100 million.
Hot 20% = 20 million × 500 bytes = 10 GB
Ten gigabytes fits in memory on a single cache node, and comfortably in a small cluster with replication. That is a striking result worth saying out loud: the entire hot set of a service serving 300,000 reads per second fits in about 10 GB. It is why this system is cache-shaped.
Bandwidth
Read egress: 300,000/s × ~500 bytes of response ≈ 150 MB/s, about 1.2 Gbps.
Write ingress: 3,000/s × 130 bytes ≈ 0.4 MB/s — negligible.
Redirect responses are tiny, so bandwidth is not a constraint here, unlike in every media system in Part III.
The API
With the numbers in hand, the design is two endpoints and one table.
POST /v1/urls body: {"long_url": "https://...", "custom_alias": "summer-sale", "expires_at": "..."} returns: 201 {"short_url": "https://short.ly/aB3xY7z", "key": "aB3xY7z"} errors: 400 invalid URL · 409 alias already taken · 429 rate limitedGET /{key} returns: 301 or 302 with Location: <long_url> errors: 404 unknown key · 410 expiredTwo details worth including because they signal experience: the create endpoint should accept an idempotency key so a retried request does not create a second link (Reliability patterns), and the redirect endpoint takes no authentication, which is what allows it to be cached at the edge.
The data model
urls short_key VARCHAR(7) PRIMARY KEY -- also the partition key long_url TEXT NOT NULL owner_id BIGINT created_at TIMESTAMP expires_at TIMESTAMP NULLWhy short_key is the primary key. The dominant operation — 300,000 times a second — is "given this key, find the long URL". Making the key the primary key and the partition key means that operation is a single-key lookup on a single shard, with no secondary index and no fan-out. Everything else in the design is subordinate to that.
The access pattern is now purely key-value, which is a strong argument for a key-value or wide-column store rather than a relational one (Databases: choosing and justifying). A relational store also works; it earns nothing here, because there are no joins and no transactions beyond a single-row insert with a uniqueness constraint.
Add a secondary index on owner_id only if listing a user's links is a requirement — and note that it is a cross-shard query when the partition key is short_key.
301 versus 302: the decision with a consequence
| 301 Moved Permanently | 302 Found | |
|---|---|---|
| Browser behaviour | Caches the redirect, often for a long time | Does not cache by default |
| Server load | Repeat visits never reach you | Every visit reaches you |
| Analytics | Repeat clicks invisible | Every click countable |
| Changing the target | The cached redirect keeps sending people to the old target | Takes effect immediately |
The trade is precise. A 301 can remove a large share of repeat traffic — the exact share depends entirely on how often the same person follows the same link, so treat any specific percentage with suspicion — but you lose those clicks from analytics and you can no longer change or disable the link for anyone who has cached it. The last point matters most: a 301 to a URL that later turns out to be malicious cannot be recalled.
Recommendation: use 302 when analytics matter or links can be edited or disabled — which is the normal case for a commercial shortener. Use 301 for a purely internal shortener where load reduction is the goal and links never change. Mention 307 Temporary Redirect as the strictly correct modern equivalent of 302 for non-GET methods.