Study interactive :: Progress tools open in the Study Hub reader.

21. Designing Systems

You've read 20 chapters on individual pieces. This one is about putting them together.

In an interview (or a real design review), nobody asks "explain consistent hashing." They say: "Design a URL shortener." You have 45 minutes. You need a process.

The four steps

Use this order every time. Skipping step 1 is the most common mistake.

1. Clarify requirements  (5–10 min)
2. Estimate scale          (5 min)
3. High-level design       (10–15 min)
4. Deep dive               (15–20 min)

Step 1: Clarify requirements

Ask questions. Write answers on the board (or say them out loud).

Functional. What does it do?

Non-functional. How well?

Don't assume. "Do we need user accounts?" changes the design a lot.

Step 2: Estimate scale

Back-of-the-envelope math from Chapter 2. You need rough numbers to pick SQL vs cache vs queue.

Example prompts you can ask the interviewer:

Even if they say "make it up," pick numbers and state them. Interviewers want to see you think in orders of magnitude.

Step 3: High-level design

Draw boxes and arrows. Name the main components. Don't dive into Redis internals yet.

Typical boxes:

Say which path is hot (usually reads) and which is cold (writes).

Step 4: Deep dive

Pick 1–2 areas the interviewer cares about, or that your math says will break:

Go deep on those. Leave other boxes as "standard replication + monitoring."


Worked example: URL shortener

Prompt: Design a service like bit.ly. Users submit a long URL and get a short link. Visiting the short link redirects to the long URL.

Clarify

Question Answer (example)
Custom short codes? Optional, user-chosen if available
Analytics? Yes, click count per link
Auth? Yes, registered users; anonymous create allowed
Link expiration? Optional TTL per link
Redirect type? HTTP 302 (temporary) is fine

Non-functional (we'll assume):

Estimate

Creates:
  100M / month ≈ 100M / (30 × 24 × 3600) ≈ 40 writes/sec
  Peak × 5 ≈ 200 writes/sec

Redirects:
  200 × 10 = 2,000 redirects/sec peak
  (round to ~2K reads/sec. Modest for a cache-heavy service)

Storage (5 years, rough):
  100M × 12 × 5 = 6B links
  ~500 bytes per row (short code, long URL, metadata)
  6B × 500 B ≈ 3 TB

3 TB fits one beefy Postgres cluster for years, but you'll still add a cache because reads dominate and latency matters.

High-level design

BrowserLoad balancerAPI serverAPI serverAPI serverRedis cachePostgres

Two APIs:

  1. POST /v1/links. Body: { "url": "https://...", "custom_code": "promo" }{ "short_url": "https://short.io/promo" }
  2. GET /{code}, 302 redirect to long URL (this is the hot path)

For analytics, don't block the redirect. Emit an event (Chapter 19):

AnalyticsKafkaAPIClientAnalyticsKafkaAPIClientGET /abc302 redirectpublish click eventupdate click counts

Deep dive 1: Generating short codes

You need a unique, short string. Options:

Approach Pros Cons
Hash long URL (MD5/base62) Deterministic, same URL → same code Collisions; can't support custom codes easily
Auto-increment ID → base62 Simple, no collisions Predictable; need a central ID generator at scale
Random string (6–8 chars) Unpredictable Must check DB for collision

Practical choice: random 7-character base62 (a-zA-Z0-9) → 62^7 ≈ 3.5 trillion possibilities. At 6B links you're fine. On collision, retry.

import secrets
import string

ALPHABET = string.ascii_letters + string.digits

def new_code(length=7):
    return "".join(secrets.choice(ALPHABET) for _ in range(length))

Custom codes: check uniqueness in Postgres before insert. Return 409 if taken.

Schema (simplified):

CREATE TABLE links (
    id           BIGSERIAL PRIMARY KEY,
    short_code   VARCHAR(16) UNIQUE NOT NULL,
    long_url     TEXT NOT NULL,
    user_id      BIGINT,
    expires_at   TIMESTAMPTZ,
    created_at   TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_links_code ON links(short_code);

Deep dive 2: The redirect path (read-heavy)

Every redirect should hit Redis first (Chapter 10):

GET short_code from Redis
  hit  → 302 to long_url
  miss → SELECT from Postgres → SET Redis → 302

Cache-aside. TTL: 24 hours (or forever until explicit delete). At 2K reads/sec, a small Redis cluster is trivial.

Why not only Postgres? 2K indexed lookups/sec is doable on one machine, but p99 latency and DB connection limits hurt under spikes. Cache keeps the DB for writes and cold keys.

Deep dive 3: Scaling beyond one region

If users are global:

CAP reminder (Chapter 17): redirects are AP. Stale cache might 302 to an old URL for seconds after an update. Acceptable for most shorteners. Use cache invalidation on update/delete.

What to mention if time allows


A second sketch: news feed (30-second version)

Interviewers often ask feed systems after URL shorteners. Same four steps:

  1. Clarify: Fan-out on write vs read? Celebrity users? Real-time?
  2. Estimate: 300M users, 500 friends avg, 10 posts/day → posts/sec and fan-out write load.
  3. High-level:
    • Post service writes to DB + publishes post.created.
    • Fan-out on write: precompute each follower's feed in Redis/DB (fast read, heavy write for celebrities).
    • Fan-out on read: merge posts at request time (cheap writes, slow reads at scale).
    • Hybrid: fan-out on write for normal users; fan-out on read for celebrities.
  4. Deep dive: feed cache key design, ranking, pagination (cursor from Chapter 9).

You don't need to finish every box. Show you know the trade-off.


Checklist before you say "we're done"

Run through this mentally:


Things to remember

Going deeper