System Design Interview

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

Two endpoints, two different systemsPOST thelong URLStore key to URLGET /abc123301 or 302redirect301 is cached by the browser and hides your analytics; 302 is not.
The write path is tiny and the read path is enormous, so they should be designed as separate systems.

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.

Five numbers, each ruling something out100 M/day / 86,4001.2 K writes/s10:1 read ratio12 K reads/s365 B rows x 500 Babout 180 TB62 to the7 is 3.5 T7 characters20 percent of a dayabout 20 GBWorkingResultWrite QPSRead QPSStorage, 10 yrKey lengthCache
Seven base-62 characters is not a style choice; it is the shortest length the ten-year row count allows.

"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:

LengthKey space
662^6 ≈ 56.8 billion — not enough
762^7 ≈ 3.52 trillion — enough, with 19× headroom
862^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.

Text
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 expired

Two 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

Text
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    NULL

Why 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 Permanently302 Found
Browser behaviourCaches the redirect, often for a long timeDoes not cache by default
Server loadRepeat visits never reach youEvery visit reaches you
AnalyticsRepeat clicks invisibleEvery click countable
Changing the targetThe cached redirect keeps sending people to the old targetTakes 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.

WRITE PATH — rareREAD PATH — 100× more trafficCLIENTCreatorVisitorPOST /shortengenerate + storeGET /{alias}look up + 301ID generatorbase62 counteralias → long URLCachehot aliaseslong URLnext idinsertshort URL1. lookup2. missreads dominate by two orders ofmagnitude, so the cache belongs on thispath only301 is cached by browsers and hidesyour click analytics; 302 is not anddoes not
Separating the paths is what makes the read side cacheable and the write side irrelevant to capacity planning.