System design guide

Design a URL shortener (TinyURL / Bitly)

The URL shortener is the most common warm-up question in system design rounds because it looks trivial and isn't. The interesting parts are generating short, unique codes at scale and serving a read-heavy workload with low latency. Here is a complete answer you can run in 45 minutes.

1. Clarify the requirements

Spend the first few minutes pinning down scope. A good set of questions: Do users need accounts? Can they pick custom aliases? Do links expire? Do we need click analytics? Are we building for a global audience?

Functional requirements

  • Given a long URL, return a short URL such as sho.rt/aZ3kP9q.
  • Visiting the short URL redirects to the original URL.
  • Optional custom alias and optional expiry time.
  • Basic click analytics (count, referrer, country) per link.

Non-functional requirements

  • Low-latency redirects: p99 under roughly 50 ms at the edge. Redirects are the hot path.
  • High availability for reads. A broken short link is a broken link on someone else's website.
  • Codes are unique and not trivially guessable (sequential IDs leak volume and let people enumerate links).
  • Analytics can be eventually consistent; a few seconds of lag is fine.

2. Back-of-the-envelope estimates

State assumptions, round aggressively, and use the numbers to drive decisions. (The method is covered in back-of-the-envelope estimation.)

QuantityAssumptionResult
Writes100M new links/day ÷ ~86,400 s~1,200/s average, ~3,500/s peak
Reads100:1 read-to-write ratio~120,000/s average, ~350,000/s peak
Storage100M/day × 365 × 5 years × ~500 bytes/row~180 billion rows, ~90 TB
Code space627~3.5 trillion codes; we use ~5% in 5 years
Hot cache~20% of a day's links get most clicks20M × 500 B ≈ 10 GB, fits in memory

The conclusions to say out loud: this is read-heavy, so the read path needs caching and should be separable from the write path; the dataset is too big for one machine, so we shard; the hot set is small, so a cache will absorb most traffic.

3. API

POST /v1/urls
  { "long_url": "https://...", "alias": "optional", "expires_at": "optional" }
  → 201 { "code": "aZ3kP9q", "short_url": "https://sho.rt/aZ3kP9q" }

GET /{code}
  → 302 Location: https://...   (404 if unknown, 410 if expired)

GET /v1/urls/{code}/stats
  → { "clicks": 1234, "by_country": {...}, "by_day": [...] }

Authenticate the create endpoint with an API key and rate-limit it per key; the redirect endpoint is public. See design a rate limiter if the interviewer pushes on abuse.

4. High-level architecture

URL shortener architecture Clients reach a load balancer and CDN edge. Create requests go to the Shorten API, which leases ID blocks from a range allocator and writes to a sharded key-value store. Redirect requests go to the redirect service, which reads from a Redis cache, falls back to the store on a miss, and publishes click events to Kafka for analytics workers. POST /urls GET /{code} lease block put get on miss async Clients Load balancer+ CDN edge Shorten API Redirect service ID rangeallocator URL store(sharded KV) Redis cache Click events(Kafka) Analyticsworkers + OLAP
Write path on top, read (redirect) path below. Click analytics is fully asynchronous.

Write path. The Shorten API validates the URL (scheme, length, a malware/blocklist check), gets a new code, and writes code → long_url to the store. It returns the short URL.

Read path. The redirect service looks the code up in Redis; on a miss it reads the store and populates the cache. It returns a 302 and publishes a click event to Kafka without waiting. Splitting reads and writes into separate services lets you scale the redirect fleet independently, which matters at a 100:1 ratio.

5. Generating short codes

This is the core of the question. There are three standard approaches, and interviewers want to hear the trade-offs.

ApproachHowProsCons
Hash + truncateMD5/SHA-256 of the URL, base62-encode, take 7 charsStateless; same URL gives same codeCollisions; you must check and retry, which needs a read before every write
Global counter + base62Increment a counter, encode itNo collisions, compact codesCounter is a bottleneck and single point of failure; codes are sequential and guessable
Pre-allocated ID rangesEach API server leases a block (e.g. 1M IDs) from a coordinator, then encodes locallyNo collisions, no per-write coordination, scales horizontallyGaps when a server dies mid-block (harmless); still sequential within a block unless you permute

A strong answer picks range allocation: a small coordinator (a row in a strongly consistent store, or ZooKeeper/etcd) hands out ID blocks; servers encode IDs locally. To stop codes being guessable, run the integer through a reversible bijective scramble (for example a Feistel network or a multiply-by-coprime-mod-627) before base62 encoding. Custom aliases bypass the generator and use a conditional insert ("put if absent") so two users can't claim the same alias.

6. Data model

urls
  code        CHAR(7)   PRIMARY KEY   -- partition key
  long_url    TEXT      NOT NULL
  owner_id    BIGINT    NULL
  created_at  TIMESTAMP
  expires_at  TIMESTAMP NULL

clicks_daily              -- written by analytics workers
  code, day, country, count   PRIMARY KEY (code, day, country)

Every lookup is by code, so shard by code using consistent hashing; with scrambled IDs, codes are uniformly distributed and shards stay balanced. If users need "list my links", add a secondary table keyed by owner_id rather than scatter-gather across shards. Raw click events stay in the log/warehouse; only aggregates live in the serving store.

7. Scaling the read path

  • Cache aggressively. Mappings are immutable (unless you allow edits), so cache-aside with a long TTL and LRU eviction is safe. A 10–20 GB Redis cluster should absorb most redirects.
  • Push popular redirects to the edge. A CDN or edge worker can serve the 302 for viral links with a short TTL, which also helps with sudden traffic spikes.
  • Replicate the store across availability zones and serve reads from replicas. Read-your-write consistency rarely matters here, but if the creator clicks their link immediately, fall back to the primary on a miss.
  • Negative caching. Cache "not found" briefly so scanners hammering random codes don't reach the database.
  • Expiry. Check expires_at on read and return 410; a background job deletes expired rows later. Don't rely on the cache to expire them for you.

8. Trade-offs to call out

  • 301 vs 302: 301 reduces load but kills analytics and makes destination changes impossible; most products choose 302.
  • Dedup the same long URL? Returning the existing code saves space but requires an index on long_url and leaks that someone else shortened it. Per-user dedup is a reasonable middle ground.
  • Synchronous vs async analytics: writing counts inline adds latency and a failure mode to every redirect; a log plus stream processing keeps redirects fast at the cost of seconds of lag.
  • Consistency: the mapping store can be AP (available, eventually consistent) because a mapping is written once; the ID allocator must be CP so blocks never overlap.

9. Common follow-up questions

  1. How do you prevent abuse (phishing, spam)? Rate-limit creation per key/IP, check URLs against a safe-browsing list at creation and periodically afterwards, and support takedowns that return a warning page.
  2. What happens if the ID allocator goes down? Servers already hold leased blocks, so writes continue for a while; run the allocator with a consensus-backed store so it fails over.
  3. How would you support multiple regions? Give each region its own ID range prefix so they never collide, replicate mappings asynchronously, and route users to the nearest region with GeoDNS.
  4. A celebrity tweets a link and it gets 1M clicks a minute. The edge cache and Redis absorb it; make sure the click pipeline is partitioned so one hot key doesn't overload a single consumer (add a random suffix to the partition key and aggregate later).
  5. How do you delete a user's data? Delete mappings by owner_id via the secondary index, purge caches, and let analytics aggregates age out according to policy.

If you want to practise this structure on other prompts, the system design interview framework lays out the same steps with timings.

FAQ

Should a URL shortener use SQL or NoSQL?

Either works. The access pattern is a single-key lookup with no joins, so a sharded key-value store (DynamoDB, Cassandra) is the natural fit at very large scale. A sharded relational database is fine and easier to reason about if you also need user accounts and per-user link lists. What matters is that you shard by short code and justify the choice.

301 or 302 redirect?

A 301 is cached by browsers, so repeat clicks never reach you: less load, but you lose click analytics and can't change the destination. A 302 (or 307) sends every click through your service. Most commercial shorteners use 302 because analytics is the product.

How long should the short code be?

Seven base62 characters give about 3.5 trillion codes, which covers 100 million new links a day for decades. Six characters (about 57 billion) is enough for smaller systems. Do the arithmetic out loud in the interview.