Forward-deployed / Learning zone
System Designa standalone module
Lesson 03

Web-scale services

TL;DR

Four systems that define "internet scale." A URL shortener maps 100 million new short links per day, using a distributed ID generator and Base62 encoding. It chooses between 301 redirects (faster, cached) and 302 redirects (trackable, measurable). A web crawler systematically downloads billions of pages, using BFS traversal, a URL frontier with politeness and priority queues, and deduplication at every layer. A notification system routes 16 million daily notifications across push (APNS/FCM), SMS (Twilio), and email (SendGrid). Each channel gets its own message queue, with templating, rate limiting, and retry logic. And a news feed system serves a personalized timeline by combining fanout-on-write for normal users with fanout-on-read for celebrities, backed by a five-layer cache architecture.

🎯 For the technical PM

Why it matters — These are the systems your product decisions directly shape. The URL shortener's redirect choice (301 vs 302) determines whether you can track click analytics. The crawler's politeness settings determine whether you get blocked by every major site. The notification system's rate limiter is the difference between engagement and user churn from spam. The news feed's fanout strategy determines whether celebrity posts take minutes or milliseconds to appear.

What it changes in your decisions — You define SLAs with numbers attached: redirect latency under 20ms, notification delivery within 30 seconds, feed refresh under 500ms. You make explicit tradeoffs between features (analytics tracking vs redirect speed) and know the engineering cost of each.

Ask your eng team — "What's our P99 notification delivery latency, and what happens to the queue when a single notification targets 10 million followers?"

Risk if ignored — Short URLs that can't be tracked because you chose the wrong redirect code. A crawler that gets IP-banned from target sites within hours. Notifications that arrive minutes late because a celebrity mention flooded the queue. A news feed where new posts take 30 seconds to appear for users following popular accounts.


URL shortener

The product: given a long URL, produce a short one (e.g., tinyurl.com/abc123). Given the short URL, redirect to the original. At 100 million new URLs per day with a 10:1 read-to-write ratio, that's 1 billion redirects per day — roughly 11,600 reads/second average, 23,000+ at peak.

The encoding decision

Two approaches to generating the short code:

Hash + collision resolution — Hash the long URL (e.g., CRC32 or MD5), take the first 7 characters. If there's a collision (another URL already mapped to that code), append a predefined string and re-hash. Problem: checking for collisions requires a database query on every write, and the collision rate grows as the table fills.

Distributed ID generator + Base62 — Generate a unique numeric ID (using the Snowflake approach from lesson 2), then encode it in Base62 (a-z, A-Z, 0-9). A 7-character Base62 string supports 62^7 = 3.5 trillion unique URLs. No collision checking needed — the ID is guaranteed unique.

The Base62 approach wins for high-throughput systems because it eliminates collision resolution and the database lookup on every write.

The redirect decision

This seemingly minor HTTP choice has major product implications:

Code Meaning Browser behavior Analytics impact
301 Moved Permanently Browser caches; future visits skip your server You lose visibility — can't count clicks after first visit
302 Found (Temporary) Browser always hits your server first Every click is tracked — essential for analytics

If click tracking and analytics are part of your product (they usually are), use 302. If pure redirect speed matters and you have no analytics requirement, use 301.

System architecture

URL shortener - data flow architecture
Write Shorten a URL
ClientPOST /shorten
→
API Gatewayauth + rate limit
→
ID GeneratorSnowflake / counter
→ base62
Base62 Encodeid → "zn9edcu"
→
DB WriteshortURL → longURL
Read Redirect (100:1 read-heavy)
ClientGET /zn9edcu
→
CacheRedis lookup
→
HIT →
302 Redirect
MISS →
DBbackfill cache
→
302 Redirect
Encoding example
2009215674938 → base62(id) → zn9edcu → tinyurl.com/zn9edcu
62 chars (a-z A-Z 0-9) · 7 chars = 627 = 3.5 trillion URLs

Write flow: Client submits a long URL. The system generates a unique ID via the distributed ID generator and converts it to Base62. It stores the (shortCode, longURL) mapping in the database and returns the short URL.

Read flow: Client visits a short URL. The system checks the cache (Redis) first. On a cache hit, it issues a 302 redirect immediately. On a cache miss, it queries the database, populates the cache, then redirects. Given the 10:1 read-to-write ratio, the cache hit rate should be high — popular short links will stay hot in cache.

Storage estimation

At 100M new URLs/day for 10 years: 365 billion records. Each record is ~100 bytes (short code + long URL + metadata). Total: ~36 TB. This is within the range of a sharded relational database or a distributed KV store partitioned via consistent hashing.


Web crawler

A web crawler systematically browses the web by downloading pages, extracting links, and following them. At scale (billions of pages), the challenge isn't downloading — it's doing so politely, efficiently, and without getting stuck in loops or traps.

Traversal strategy

BFS is preferred over DFS because DFS can get trapped in deep link chains on a single domain (forums, paginated archives, infinite calendars). BFS explores breadth-first, discovering more domains and more diverse content early.

URL frontier architecture

The URL frontier is the heart of the crawler — a sophisticated queue that manages which URLs to crawl next, balancing politeness, priority, and freshness.

The URL frontier

Priority queues + per-host politeness scheduling

Not all URLs are equal, and hammering one host gets you blocked.

Seed URLs
↓
URL Frontier — Priority Queues (PageRank, freshness) → Politeness Scheduler (per-host delay)
↓
Downloader — DNS cache, timeouts
↓
HTML Parser
↓
Content Dedup — checksums/simhash
↓
URL Extractor → URL Filter (robots.txt, blocklist)
URL seen? No → back to the Frontier · Yes → discard

Priority

Not all URLs are equal. The frontier maintains priority queues that rank URLs by:

A prioritizer assigns each URL a priority score and routes it to the appropriate queue. The front-end selector picks from higher-priority queues more frequently.

Politeness

Hammering a single domain will get you blocked. The politeness layer ensures:

This means the crawler maintains hundreds of thousands of per-host queues. A queue-router maps each URL's hostname to its queue, and worker threads pull from different host queues in parallel.

Deduplication

Deduplication happens at two levels:

  1. URL dedup — a set (often backed by a Bloom filter for memory efficiency) tracks which URLs have been seen. Before adding a URL to the frontier, check the set. This prevents crawl loops (A links to B, B links to A).

  2. Content dedup — different URLs can serve identical content (mirrors, syndication, URL parameters). Compute a content hash (or simhash for near-duplicate detection) and skip pages whose content matches a previously crawled page.

Robustness

Production crawlers handle dozens of edge cases:


Notification system

The product: deliver 10 million mobile push notifications, 1 million SMS messages, and 5 million emails per day — 16 million total, across three channels with different providers, latency expectations, and failure modes.

Provider landscape

Each channel has its own delivery infrastructure:

Channel Provider Protocol Characteristics
iOS push APNS (Apple) HTTP/2, certificate-based Token-based auth, payload ≤ 4 KB
Android push FCM (Google) HTTP/JSON API-key auth, payload ≤ 4 KB
SMS Twilio, Nexmo REST API Per-message cost, carrier variability
Email SendGrid, SES SMTP/API Deliverability reputation, bounce handling

Architecture: per-channel message queues

The critical design insight: each channel gets its own message queue and worker pool. This prevents a slow channel (SMS, which depends on carrier networks) from blocking a fast channel (push, which is near-instant).

Per-channel message queues

A slow channel never blocks a fast one

SMS depends on carrier networks; push is near-instant — each gets its own queue and workers.

Service / Trigger
→
Notification Service — validation, templating
→
User preference cache
Push Queue
Push Workers — APNS / FCM
SMS Queue
SMS Workers — Twilio
Email Queue
Email Workers — SendGrid
All three workers → Event Tracking

Notification templates

At 16M notifications/day, you don't craft individual messages. Templates define the structure:

Hello {{user.first_name}}, your order {{order.id}} has shipped!
Tracking: {{tracking.url}}

Templates are stored, versioned, and cached. The notification service merges the template with user-specific data at send time. This enables A/B testing (different templates for different cohorts), localization (language-specific templates), and rapid iteration without code deployments.

Rate limiting

Without rate limiting, a bug or misconfigured campaign can blast millions of users with duplicate notifications — the fastest way to destroy app engagement. The notification system applies rate limits at multiple levels:

The rate limiter uses the same algorithms from lesson 2 — typically sliding window counter backed by Redis.

Retry and failure handling

Notifications fail: APNS returns an invalid token, Twilio gets a carrier error, SendGrid bounces an email. The system must:

  1. Retry with exponential backoff — first retry after 1s, then 2s, 4s, 8s, up to a maximum (e.g., 1 hour)
  2. Dead letter queue — after n retries, move the notification to a DLQ for manual investigation
  3. Token invalidation — if APNS returns "invalid token," remove the device token from the user's record to stop future failures
  4. Bounce handling — hard email bounces (invalid address) should permanently suppress the address; soft bounces (mailbox full) can be retried

Event tracking

The notification system tracks the full lifecycle:

This data feeds analytics dashboards and enables product decisions: which notification types drive engagement, which channels perform best for which user segments, when to send for maximum open rates.


News feed system

The product: show each user a personalized timeline of posts from people they follow. Facebook's news feed, Twitter's timeline, Instagram's feed. The core challenge: when a user with 10 million followers posts, how do you make that post appear in all 10 million feeds quickly?

The fanout problem

Two fundamental approaches:

Fanout on write (push model) — When a user publishes a post, immediately write it to every follower's feed cache. The feed is pre-computed. Reading is instant — just fetch the cache.

Fanout on read (pull model) — When a user opens their feed, the system fetches posts from all followed users on the fly, merges, ranks, and returns.

Hybrid approach (the production answer): Use fanout-on-write for most users (fast reads for the common case). For users with massive followings (celebrities, verified accounts), use fanout-on-read — their posts are fetched and merged into the feed at read time rather than pushed to millions of caches.

Fanout on write vs. fanout on read

Hybrid: push for normal users, pull for celebrities

A user with 10M followers can't trigger 10M cache writes on every post.

Fanout on write — normal users
User posts
→
Fanout Service
↔
Social Graph — get followers
Fanout Service →
Follower 1 feed cache
Follower 2 feed cache
Follower N feed cache
Fanout on read — celebrities
Feed request
→
Fetch Service
→
Celebrity posts store
→
Merge with cached feed

Feed publishing flow

  1. User creates a post via the API
  2. Post is stored in the post database and the post cache
  3. The fanout service queries the social graph cache for the user's followers
  4. For each follower, the post ID is inserted into their feed cache (a sorted set in Redis, ordered by timestamp or ranking score)
  5. If the poster is a celebrity (follower count above threshold), skip fanout — the post stays only in the post store

Feed retrieval flow

  1. User opens their feed
  2. The system reads the user's feed cache (pre-computed post IDs)
  3. For each celebrity the user follows, fetch their recent posts from the post store
  4. Merge the cached feed with celebrity posts
  5. Rank (by time, engagement, ML model) and return the top N posts
  6. For each post ID, hydrate the full content from the post cache and user cache (author name, avatar)

The five-layer cache

Feed performance depends entirely on cache architecture. Production news feed systems use five distinct cache layers:

Cache layer What it stores Why it's separate
News feed cache Pre-computed feed (list of post IDs per user) The hottest data — every feed load hits this
Content cache Post bodies, images, metadata Large objects; different eviction pattern from feed IDs
Social graph cache Follow/friend relationships Queried on every fanout; rarely changes
Action cache Likes, comments, shares per post High write rate; separate to avoid invalidating content cache
Counter cache Like counts, comment counts, share counts Extremely high write rate; often approximate

Each layer has its own cluster, eviction policy, and TTL. Mixing them in a single cache would cause hot data (counters) to evict warm data (social graph), degrading overall hit rates.

Social graph cache

The social graph (who follows whom) is read on every fanout and every feed fetch. It changes rarely (follows/unfollows are infrequent compared to posts and reads). Storing it in a dedicated cache with long TTLs and lazy invalidation keeps fanout latency low. The data structure is typically a hash map: user_id → set of follower_ids.


Failure modes

Practitioner checklist