Introduction#
Every system you design will use some combination of infrastructure building blocks. Understanding what each component does, how it works internally, when to use it, trade-offs, and how it fits with other components is essential for system design interviews and real-world architecture.
This document provides deep coverage of each component — not just definitions, but the practical knowledge you need to make informed design decisions.
%% title: The system design building blocks
flowchart TD
E@{ icon: "sd:globe", label: "Edge layer<br/>CDN · WAF · DNS<br/>Load Balancer · API Gateway" }
A["Application layer<br/>Web Servers · Application Servers<br/>Microservices"]:::core
D@{ icon: "sd:database", label: "Data layer<br/>Databases · Caches<br/>Object Storage · Search Engines" }
M@{ icon: "sd:queue", label: "Messaging layer<br/>Message Queues · Event Streams<br/>Pub/Sub" }
E --> A --> D --> M
class E edge
class D store
class M queueLayer by layer: the edge (a CDN cuts a 200ms trip from Tokyo to about 20ms, DNS resolves names, a load balancer spreads 50,000 requests a second and drops servers that fail health checks), the application layer (stateless servers with sessions in Redis, monolith first), the data layer (PostgreSQL by default, cache-aside at a 5.9ms average, S3 for files) and messaging (an event bus, work queues and pub-sub instead of a synchronous chain).
Chapter 1: Edge Layer Components#
The edge layer is the first thing users interact with. It sits between users and your application servers, handling traffic before it reaches your backend. A well-designed edge layer dramatically improves performance, security, and reliability.
1.1 Content Delivery Network (CDN)#
What Is a CDN?#
A Content Delivery Network is a geographically distributed network of proxy servers (called edge servers or Points of Presence - PoPs). The goal is to serve content from locations physically closer to users, reducing latency dramatically.
When a user requests content, instead of traveling thousands of miles to your origin server, they connect to the nearest CDN edge location.
Estimate it: a Tokyo user with and without the edge
A user in Tokyo loads images from a US origin 200 ms away. A Tokyo CDN edge answers a cached image in ~20 ms, and a miss costs ~220 ms (edge plus the trip to the origin). If 95% of requests hit the edge cache, what is the average latency, and what share of requests still reaches the origin?
One strong answer
- Average: 0.95 × 20 ms + 0.05 × 220 ms = 19 + 11 = ~30 ms, against 200 ms+ for every request without a CDN.
- Origin: only the 5% of misses, so the origin handles a twentieth of the traffic.
- The misses still cost more than going direct, which is why the hit ratio is the number to watch.
How CDN Works — Step by Step#
%% title: CDN request flow sequenceDiagram participant U as User (Tokyo) participant E as CDN Edge (Tokyo) participant O as Origin (US-East) Note over U,O: SCENARIO · User in Tokyo requests image from US-based origin Note over U,O: WITHOUT CDN U->>O: Request · 200ms RTT O-->>U: Response Note over U,O: Every single request = 200ms+ latency<br/>Origin server handles ALL traffic worldwide<br/>High bandwidth costs for origin Note over U,O: WITH CDN · first request (cache miss) U->>E: 1. Request image.jpg from nearest CDN edge (Tokyo) E->>E: 2. Check local cache → MISS (not cached yet) E->>O: 3. Fetch from origin server (200ms to US) O-->>E: 4. Origin returns image to CDN edge E-->>U: 5. Cache the image AND return to user Note over U,O: First request total time ~220ms (still slow, but now cached!) Note over U,E: WITH CDN · subsequent requests (cache hit) U->>E: 1. Request image.jpg E-->>U: 2. Return from local cache immediately Note over U,E: No origin request needed!<br/>Subsequent requests ~20ms (10x faster!)<br/>Origin load reduced by 90%+ for popular content
CDN Points of Presence (PoPs)#
Major CDN providers have hundreds of edge locations worldwide:
Cloudflare network (Example)
- North America: 55+ cities (LA, NYC, Chicago, Dallas, Seattle...)
- Europe: 55+ cities (London, Frankfurt, Paris, Amsterdam...)
- Asia Pacific: 50+ cities (Tokyo, Singapore, Sydney, Mumbai...)
- South America: 20+ cities (São Paulo, Buenos Aires...)
- Africa: 15+ cities (Johannesburg, Cape Town, Lagos...)
- Middle East: 10+ cities (Dubai, Tel Aviv...)
Total: 330+ cities across 100+ countries (as of 2025-26)
AWS CloudFront
- 750+ Points of Presence
- 15 Regional Edge Caches
- Integrated with AWS services
The closer the PoP to the user, the lower the latency.
- A user in Mumbai connecting to a Mumbai PoP = ~5-10ms
- Same user connecting to US origin = ~150-200ms
What to Cache vs What NOT to Cache#
This is critical for CDN strategy:
Always cache (Static Content)
- Images
- Product photos, thumbnails, hero images
- User avatars (public ones)
- Icons, logos, favicons
- Formats: jpg, png, gif, webp, svg, ico
- TTL: 1 year (use versioned URLs for updates)
- CSS & JavaScript
- Stylesheets (
app.css) - JavaScript bundles (
app.js,vendor.js) - Use content hash in filename:
app.a1b2c3d4.js - TTL: 1 year (hash changes = new URL = cache miss = fresh file)
- Stylesheets (
- Fonts
- Web fonts: woff, woff2, ttf, eot
- TTL: 1 year (fonts rarely change)
- Video & audio
- Video files: mp4, webm
- Audio files: mp3, wav
- HLS/DASH segments for streaming
- TTL: 1 year or based on content type
- Documents
- PDFs, downloadable files
- Software installers
- TTL: Based on update frequency
Cache with care (Semi-Dynamic Content)
- API responses (only if SAME for all users)
- ok: Product catalog (same for everyone)
- ok: Store locations
- ok: Public configuration
- no: User profile (different per user!)
- TTL: 1-5 minutes (short, to stay fresh)
- Search results
- Only if you expect high cache hit rate
- Popular searches might benefit
- TTL: 1-5 minutes
- User-agnostic pages
- Homepage (logged-out version only)
- Category/listing pages
- Marketing/landing pages
- TTL: 5-60 minutes
Never cache
- User-specific data
- User profiles, account pages
- Shopping carts, wishlists
- Order history
- Personalized recommendations
- Dashboard data
- Authenticated API responses
- Anything requiring login
- User-specific search results
- Private data
- Write operations
- POST requests
- PUT/PATCH requests
- DELETE requests
- Real-time data
- Stock prices, cryptocurrency rates
- Live sports scores
- Chat messages
- Live analytics dashboards
- Webhooks & callbacks
- Payment confirmations
- Third-party notifications
- OAuth callbacks
- Sensitive data
- Financial information
- Health records (HIPAA)
- PII (Personally Identifiable Information)
Which cache should keep it longer: the CDN or the browser?
A page is the same for every user and changes a few times a day. You can give the CDN and the browser different cache times. Which one gets the longer TTL, and why?
One strong answer
- The CDN. You can purge a CDN when the content changes, but you can't reach into users' browsers to clear their copies.
- So the browser keeps a short TTL and checks back often, and the CDN keeps a long one and is purged on change.
- That's the
max-age=3600, s-maxage=86400pattern below: 1 hour in the browser, 24 hours at the CDN.
Cache-Control Headers Deep Dive#
HTTP headers control how CDNs (and browsers) cache content:
Long-term caching (Static assets with content hash)
Cache-Control: public, max-age=31536000, immutable
public— Can be cached by CDN and browsermax-age=31536000— Cache for 1 year (365 * 24 * 60 * 60)immutable— Don't even check for updates (save revalidation)
Use for: app.a1b2c3.js, style.x7y8z9.css, image.abc123.png (Files with content hash in filename - changing content = new file)
Short-term caching (Semi-dynamic content)
Cache-Control: public, max-age=300, must-revalidate
max-age=300— Cache for 5 minutesmust-revalidate— After TTL, MUST check with origin before serving
Use for: API responses, product listings, semi-dynamic pages
Revalidation caching (Always fresh, but allow caching)
Cache-Control: public, no-cache · ETag: "abc123"
no-cache— DOESN'T mean "don't cache"!- It means: Cache it, but revalidate with origin before using
- ETag allows conditional requests (
If-None-Match: "abc123") - If unchanged, origin returns 304 Not Modified (no body = fast)
Use for: Frequently updated content where staleness is unacceptable
No caching (Private/sensitive content)
Cache-Control: private, no-store
private— Only browser can cache (not CDN)no-store— Don't cache at all, not even temporarily
Use for: User-specific data, authenticated responses, sensitive data
Different TTL for CDN vs browser
Cache-Control: public, max-age=3600, s-maxage=86400
max-age=3600— Browser caches for 1 hours-maxage=86400— CDN (shared cache) caches for 24 hours
Why? CDN can invalidate quickly. Browsers can't. So CDN can cache longer, browser should check more often.
Stale-while-revalidate (Performance optimization)
Cache-Control: public, max-age=300, stale-while-revalidate=86400
- For 5 minutes: Serve from cache
- For up to 24 hours after that: Serve stale content IMMEDIATELY, but fetch fresh content in background
- After that window: Must wait for fresh content
Benefit: User never waits for revalidation!
Cache Invalidation Strategies#
1. Versioned URLs (Recommended for static assets)
- Instead of:
/static/app.js - Use:
/static/app.a1b2c3d4.js(content hash in filename)
How it works:
- Build tool (Webpack, Vite) generates hash from file content
- When code changes, hash changes, URL changes
- New URL = automatic cache miss = users get fresh file
- Old version stays cached (harmless, eventually expires)
Pros
- Instant updates for users
- Zero cache purge latency
- Safe rollbacks (old URLs still work)
- No CDN API calls needed
2. Purge API (For non-versioned content)
AWS CloudFront:
aws cloudfront create-invalidation \
--distribution-id EDFDVBD632 \
--paths "/images/*" "/api/products/*"Cloudflare:
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer {token}" \
-d '{"files":["https://example.com/image.jpg"]}'Propagation time:
| CDN | Propagation |
|---|---|
| CloudFront | Typically under ~2 minutes; can take 10-15 minutes (must propagate to all 750+ PoPs) |
| Fastly | ~150ms (instant purge is their specialty) |
| Cloudflare | <150ms on average (Instant Purge, rebuilt 2022-2024) |
Pros
- Works for any URL pattern
Cons
- Propagation delay
- May have cost (CloudFront: 1000 free/month, then $0.005 each)
3. TTL-based expiry (Simple but has staleness window)
Cache-Control: max-age=300 (5 minutes)
- Content cached for 5 minutes
- After TTL, CDN checks with origin (
If-Modified-Since) - Origin returns 304 (unchanged) or 200 (new content)
Trade-off:
- Short TTL (60s) = More origin requests, always fresh
- Long TTL (1hr) = Fewer origin requests, may be stale
4. Cache tags / surrogate keys (Advanced)
Supported by: Fastly, Varnish, some CDNs
Response header: Surrogate-Key: product-123 category-shoes brand-nike
When product 123 updates: PURGE /tags/product-123
- → Invalidates ALL URLs tagged with "product-123"
- → Product page, category pages, search results, recommendations...
Pros
- Granular invalidation without knowing all URLs
- One purge affects all related content
Cons
- Requires CDN support
Best practice: Combine strategies
| Content | Strategy |
|---|---|
| Static assets (JS, CSS, images) | Versioned URLs + long TTL |
| API responses | Short TTL + Purge on update |
| HTML pages | stale-while-revalidate + Purge on publish |
CDN Provider Comparison#
CloudFront (AWS)
Network: 750+ edge locations, 15 regional edge caches
Strengths
- Deep AWS integration (S3, ALB, API Gateway, Lambda@Edge)
- Lambda@Edge for compute at edge (auth, redirects, A/B testing)
- Origin Shield (extra caching layer to protect origin)
- Real-time logs to Kinesis
- Field-level encryption
- WebSocket support
Weaknesses
- Slower invalidation (usually minutes, not milliseconds)
- Complex pricing model
- Configuration can be verbose
Best for: AWS-native applications, serverless architectures
Pricing: ~$0.085/GB (varies by region), $0.0075/10K HTTP requests ($0.01/10K HTTPS)
Cloudflare
Network: 330+ cities worldwide
Strengths
- Generous FREE tier (yes, really free)
- Built-in DDoS protection (industry-leading)
- Workers (JavaScript/WASM at edge - very powerful)
- Workers KV (distributed key-value at edge)
- Instant cache purge (<150ms global average)
- Easy setup (just change DNS nameservers)
- Fast public DNS resolver (1.1.1.1, often top-ranked in benchmarks)
- Argo Smart Routing (optimized paths)
Weaknesses
- Less control over caching behavior than some competitors
- Advanced features require paid plans
- Must use Cloudflare DNS on Free/Pro plans (CNAME setup needs Business+; can be a dealbreaker)
Best for: Startups, security-focused apps, edge computing
Pricing: Free tier, Pro $20/mo, Business $200/mo (billed annually; $25 and $250 billed monthly), Enterprise custom
Fastly
Network: 100+ PoPs (fewer but very high capacity)
Strengths
- INSTANT PURGE (<150ms global) - their killer feature
- VCL (Varnish Configuration Language) for custom caching logic
- Real-time analytics and logging
- Fastly Compute (formerly Compute@Edge; WebAssembly at edge)
- Image optimization built-in
- Surrogate keys for granular invalidation
Weaknesses
- Fewer PoPs than competitors (100+ vs 330+)
- Steeper learning curve (VCL is powerful but complex)
- No free tier (only a $50 developer trial credit)
- Higher cost for small sites
Best for: Media streaming, real-time content, advanced caching needs
Pricing: $0.12/GB + $0.0075/10K requests (minimum ~$50/mo)
Akamai
Network: 4,000+ locations (largest network by far)
Strengths
- Largest, most distributed network in the world
- Enterprise-grade SLAs (99.99%+)
- Advanced security (WAF, Bot Management, DDoS)
- Media delivery specialization (used by major streaming services )
- API acceleration
Weaknesses
- Expensive (enterprise pricing)
- Complex configuration and management
- Sales-driven (hard to get started without talking to sales)
Best for: Large enterprises, media companies, global reach needs
Pricing: Enterprise contracts (typically $$$$$)
Quick selection guide
| Situation | Pick |
|---|---|
| AWS shop / serverless | CloudFront |
| Startup / budget-conscious | Cloudflare (free tier) |
| Need instant cache purge | Fastly or Cloudflare |
| Edge computing / Workers | Cloudflare |
| Enterprise / media streaming | Akamai or Fastly |
| Simple setup | Cloudflare |
CDN Interview Tips#
When discussing CDN in system design interviews:
What to mention
- What you're caching — "Static assets like images, CSS, JS, and fonts are served through CDN." "For API responses that are identical for all users, like product catalog, we can also cache at the CDN."
- Cache strategy — "We use content-hashed filenames (app.a1b2c3.js) which allows us to set an aggressive 1-year TTL. When code changes, the hash changes, so users automatically get the new version."
- Cache hit ratio — "We target 95%+ cache hit ratio for static assets. This reduces origin load significantly."
- Geographic distribution — "With users in Asia, Europe, and Americas, CDN ensures everyone gets sub-50ms latency for static content instead of 200ms+ to our US origin."
- Cache invalidation — "For product data that changes, we use short TTL (5 min) combined with cache purge on updates for immediate consistency when needed."
Sample interview answer
"For static content, we'll use CloudFront CDN. All JavaScript and CSS files use content-based hashing in filenames, like app.a1b2c3.js, which lets us set a 1-year cache TTL. When we deploy new code, the hash changes, so users automatically get fresh files.
For images, we serve them from S3 through CloudFront with the same long TTL. This gives us 95%+ cache hit ratio and reduces our origin bandwidth by about 90%.
For semi-dynamic content like product listings that are the same for all users, we cache at the CDN with a 5-minute TTL. When a product is updated, we purge that specific path.
With this setup, users globally get sub-50ms response times for static content, and our origin servers only handle cache misses and authenticated requests."
Further Reading: CDN#
- Docs
Cloudflare Learning Center: https://www.cloudflare.com/learning/cdn/what-is-a-cdn/
- Docs
AWS CloudFront Developer Guide: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/
- Docs
Fastly Documentation: https://docs.fastly.com/
- Read
Web.dev - HTTP Caching: https://web.dev/http-cache/
- Blog
High Scalability - CDN Architecture: http://highscalability.com/blog/2011/2/28/a-practical-guide-to-building-a-content-delivery-network-c.html
1.2 Load Balancer#
What Is a Load Balancer?#
A load balancer distributes incoming network traffic across multiple servers. It's one of the most fundamental components for building scalable systems.
Why do we need it?
- Scalability: One server can only handle so much. Distribute load across many.
- Availability: If one server dies, others continue serving traffic.
- Performance: Route to the least-loaded or fastest-responding server.
What breaks if one of three servers dies?
A load balancer spreads 50,000 requests a second over three identical servers. Each server can handle at most 20,000 requests a second. One server crashes and the health checks remove it. What happens next?
One strong answer
- Before: ~17,000 a second each, inside the 20,000 limit.
- After: 50,000 ÷ 2 = 25,000 a second each, more than either survivor can take, so they overload too.
- Removing a dead server only helps if the rest have room for its share. Size the pool so N − 1 servers can carry the whole load.
How Load Balancing Works#
%% title: Load balancer architecture
flowchart TD
T@{ icon: "sd:users", label: "Incoming traffic<br/>(50,000 req/sec)" } --> LB@{ icon: "sd:load-balancer", label: "LOAD BALANCER<br/>Listener (Port 443) · Routing Rules · Health Checks<br/>SSL Termination · Session Persistence · Algorithm Selection" }
LB --> S1@{ icon: "sd:server", label: "Server 1<br/>~17K req/s · OK" }
LB --> S2@{ icon: "sd:server", label: "Server 2<br/>~17K req/s · OK" }
LB --> S3@{ icon: "sd:server", label: "Server 3<br/>~17K req/s · DOWN" }
HC["Health check probes<br/>(every 10-30 seconds)"]:::note -.-> S1 & S2 & S3
class T client
class LB edge
class S1,S2 ok
class S3 bad- Server 3 is marked unhealthy → traffic redistributed to 1 & 2
- Now: Server 1 and 2 each handle ~25K req/s
Load Balancing Algorithms — Detailed#
1. Round robin
How it works:
- Request 1 → Server A
- Request 2 → Server B
- Request 3 → Server C
- Request 4 → Server A (cycle repeats)
- Request 5 → Server B
- ...
Implementation: next = servers[counter % len(servers)]; counter++
Pros
- Simple to implement and understand
- Even distribution over time
- No state to maintain
- Works well with homogeneous servers
Cons
- Ignores server capacity (all treated equal)
- Ignores current load
- Some requests may take longer than others
Best for: Identical servers, stateless requests, similar workloads
2. Weighted round robin
| Server | Configuration | Distribution (out of 10 requests) |
|---|---|---|
| Server A | weight = 5 (more powerful, 8 CPU cores) | 5 requests (50%) |
| Server B | weight = 3 (medium, 4 CPU cores) | 3 requests (30%) |
| Server C | weight = 2 (smaller, 2 CPU cores) | 2 requests (20%) |
Pros
- Accounts for different server capacities
- Useful during migrations (old=1, new=5)
- Canary deployments (stable=99, canary=1)
Cons
- Weights are static, don't adapt to actual load
- Requires manual tuning
Best for: Heterogeneous servers, gradual rollouts, canary deploys
3. Least connections
Current state:
- Server A: 150 active connections
- Server B: 75 active connections ← Next request goes here
- Server C: 120 active connections
How it works:
- LB tracks active connections per server
- New request → server with fewest connections
- Connection count incremented when request starts
- Connection count decremented when response completes
Pros
- Automatically adapts to actual load
- Handles varying request durations well
- Self-balancing
Cons
- Requires connection tracking (slight overhead)
- Doesn't account for connection "weight" (some heavier than others)
Best for: Long-running connections, WebSockets, variable workloads
4. Least response time
Current metrics:
- Server A: avg response = 45ms, 50 connections
- Server B: avg response = 28ms, 60 connections ← Fastest!
- Server C: avg response = 52ms, 40 connections
Calculation: Route to server with lowest (response_time * connections)
Pros
- Optimizes for actual user-perceived latency
- Automatically adapts to server performance
- Accounts for both load and speed
Cons
- More complex to implement
- Can be "noisy" with variable latencies
- May starve consistently slow servers
Best for: Latency-sensitive applications, heterogeneous performance
5. IP hash (Source IP Affinity)
How it works: server_index = hash(client_ip) % number_of_servers
| Client | hash(ip) | % 3 | Server |
|---|---|---|---|
| 192.168.1.1 | 12345 | 12345 % 3 = 0 | Server A |
| 192.168.1.2 | 67890 | 67890 % 3 = 0 | Server A |
| 192.168.1.3 | 11111 | 11111 % 3 = 2 | Server C |
Same client always goes to same server (until server pool changes)
Pros
- Sticky sessions without cookies
- Good for local server-side caching
- Stateless LB (no session table needed)
Cons
- Can cause uneven distribution
- Adding/removing server reshuffles many clients
- Mobile users may change IP (WiFi → cellular)
- Users behind NAT share IP
Best for: Caching layers, when cookies aren't available
6. Consistent hashing (Advanced)
How it works:
- Servers placed on a virtual ring (hash space 0 to 2^32 - 1)
- Request key hashed to point on ring
- Route to first server clockwise from that point
- On the ring: Server A → Server B → Server C (clockwise)
Key benefit: Adding/removing server only affects 1/N of keys! (Unlike IP hash where many clients get reshuffled)
Best for: Distributed caches, databases, when servers change often
Algorithm selection guide
| Scenario | Recommended Algorithm |
|---|---|
| Simple stateless API | Round Robin |
| Mixed server sizes | Weighted Round Robin |
| Long-running connections | Least Connections |
| Latency-sensitive app | Least Response Time |
| Need session stickiness | IP Hash or Cookie-based |
| Distributed cache | Consistent Hashing |
| Canary deployment | Weighted Round Robin |
Default choice: Least Connections (adapts well to most scenarios)
Round robin sends requests to A, B, C in turn, ignoring load. Weighted round robin with weights 5, 3 and 2 gives A 5, B 3 and C 2 of every 10 requests. Least connections sends each new request to the server with the fewest active connections (B, with 75 against 150 and 120).
Balancing PostgreSQL connections: Layer 4 or Layer 7?
Your app servers open connections to a pool of PostgreSQL read replicas, and you want a load balancer in front of them. Should it work at Layer 4 or Layer 7?
One strong answer
- Layer 4. The database protocol isn't HTTP, so a Layer 7 balancer has no paths, headers or cookies to route on.
- Layer 4 only needs the IP address and port, works for any TCP protocol and adds very little latency.
- Layer 7 is worth its extra cost only when you need content-based routing, such as sending
/api/v2somewhere else.
Layer 4 vs Layer 7 Load Balancing#
OSI model reference: Layer 4 = Transport Layer (TCP/UDP) · Layer 7 = Application Layer (HTTP/HTTPS)
Layer 4 (L4)
What it sees — TCP/IP Packet Headers:
- Source IP: 192.168.1.100
- Destination IP: 10.0.0.1
- Source Port: 52431
- Destination Port: 443
- Protocol: TCP
- [Payload is encrypted/opaque - LB cannot read it]
What it can do
- Route based on IP address and port
- Very fast (no packet inspection, just forward)
- Protocol agnostic (TCP, UDP, any port)
- Preserve client IP (DSR/Direct Server Return mode)
- Handle millions of connections
What it cannot do
- Route based on URL path (
/apivs/static) - Read HTTP headers or cookies
- Modify request/response content
- Smart routing based on content
- SSL termination (usually passed through; AWS NLB can terminate TLS)
Use cases: Database connections (MySQL, PostgreSQL) · Redis, Memcached · Gaming servers (UDP) · Any non-HTTP TCP/UDP service · When you need maximum performance
AWS: Network Load Balancer (NLB) · GCP: TCP/UDP Load Balancer
Performance: ~1M+ connections/sec, <100μs added latency
Layer 7 (L7)
What it sees (after SSL termination) — Full HTTP Request:
- Method: GET
- Path:
/api/v2/users/123 - Host:
api.example.com - Headers:
Authorization: Bearer eyJhbGc...Content-Type: application/jsonX-Request-ID: abc123X-Feature-Flag: new-checkout
- Cookies:
session_id=xyz789
- Body:
{"name": "John"}
What it can do
- Route based on URL path (
/api/*→ API servers) - Route based on hostname (
api.*vswww.*) - Route based on HTTP headers
- Route based on cookies (session affinity)
- Route based on query parameters
- SSL/TLS termination
- Add/modify/remove headers
- URL rewriting (
/old-path→/new-path) - Response caching
- Compression (gzip)
- Rate limiting
- Authentication
- WebSocket support
Example L7 routing rules:
| Rule | Condition | Route to |
|---|---|---|
| Rule 1 | IF path starts with /api/v1/* |
api-v1-target-group |
| Rule 2 | IF path starts with /api/v2/* |
api-v2-target-group |
| Rule 3 | IF path starts with /static/* |
static-servers |
| Rule 4 | IF header X-Canary equals "true" |
canary-target-group |
| Rule 5 | IF host equals admin.example.com |
admin-servers |
| Default | — | web-servers |
Use cases: Web applications · REST APIs · Microservices routing · A/B testing (route 10% to new version) · Canary deployments · Multi-tenant applications (route by subdomain) · Blue-green deployments
AWS: Application Load Balancer (ALB) · GCP: HTTP(S) Load Balancer
Performance: ~100K connections/sec, 1-5ms added latency
Comparison summary
| Aspect | Layer 4 (NLB) | Layer 7 (ALB) |
|---|---|---|
| Operates at | TCP/UDP level | HTTP/HTTPS level |
| Sees | IP + Port | Full HTTP request |
| Speed | Very fast | Fast |
| Latency added | <100μs | 1-5ms |
| SSL termination | Usually pass-through (NLB TLS listeners can terminate) | Yes |
| Content routing | No | Yes |
| Header manipulation | No | Yes |
| WebSocket | Yes (pass-through) | Yes (native) |
| Health checks | TCP/HTTP | HTTP with path |
| Cost (AWS) | Lower | Higher |
When to use which:
- HTTP/HTTPS traffic → L7 (ALB)
- Database connections → L4 (NLB)
- Need content-based routing → L7
- Need maximum throughput → L4
- Non-HTTP protocols → L4
Health Checks Deep Dive#
Purpose: Detect unhealthy servers and stop routing traffic to them
Configuration example (AWS ALB)
| Setting | Value |
|---|---|
| Protocol | HTTP |
| Port | 8080 |
| Path | /health |
| Interval | 30 seconds |
| Timeout | 5 seconds |
| Healthy threshold | 2 consecutive successes |
| Unhealthy threshold | 3 consecutive failures |
| Success codes | 200-299 |
Health check timeline example
| Time | LB Sends | Server Response | Server Status |
|---|---|---|---|
| 0s | GET /health |
200 OK | Starting (1/2) |
| 30s | GET /health |
200 OK | HEALTHY (2/2) ← Active! |
| 60s | GET /health |
200 OK | Healthy |
| 90s | GET /health |
500 Error | Warning (1/3 failures) |
| 120s | GET /health |
Connection timeout | Warning (2/3 failures) |
| 150s | GET /health |
500 Error | UNHEALTHY ← Removed! |
| 180s | GET /health |
200 OK | Recovering (1/2) |
| 210s | GET /health |
200 OK | HEALTHY ← Back in pool! |
Types of health checks
1. Shallow / liveness check — Just verifies the process is running and can respond
GET /health Response:
{
"status": "ok"
}Pros
- Fast, low overhead
- Always succeeds if process is up
- Good for load balancer health checks
Cons
- Doesn't verify dependencies work
2. Deep / readiness check — Verifies the app AND all its dependencies are healthy
GET /health/ready Response (healthy):
{
"status": "healthy",
"version": "1.2.3",
"uptime_seconds": 86400,
"checks": {
"database": {
"status": "up",
"latency_ms": 5
},
"redis": {
"status": "up",
"latency_ms": 1
},
"external_api": {
"status": "up",
"latency_ms": 45
}
}
}Response (unhealthy - HTTP 503):
{
"status": "unhealthy",
"checks": {
"database": {
"status": "down",
"error": "Connection refused"
},
"redis": { "status": "up" },
"external_api": { "status": "up" }
}
}Pros
- Comprehensive status
- Catches dependency failures
Cons
- Slower (queries dependencies)
- Risk: If DB is down, ALL servers marked unhealthy!
Best practice
Use SHALLOW check for load balancer (keep traffic flowing). Use DEEP check for monitoring/alerting (know what's broken).
| Endpoint | Checked by |
|---|---|
/health |
LB checks this (fast, always works if process up) |
/health/ready |
Kubernetes readiness probe |
/health/detailed |
Monitoring systems (Datadog, etc.) |
Load Balancer Providers#
Cloud load balancers
AWS
- ALB (Application Load Balancer) - Layer 7
- HTTP/HTTPS traffic
- Path-based and host-based routing
- WebSocket and HTTP/2 support
- Integrated with WAF, Cognito
- ~$0.0225/hour + $0.008/LCU
- NLB (Network Load Balancer) - Layer 4
- TCP/UDP/TLS traffic
- Ultra-low latency (<100μs)
- Millions of requests per second
- Static IP / Elastic IP support
- ~$0.0225/hour + $0.006/NLCU
GCP
- HTTP(S) Load Balancer - Global L7
- Single anycast IP worldwide
- Automatic multi-region failover
- Cloud CDN integration
- Network Load Balancer - Regional L4
- High-performance TCP/UDP
- Regional scope
Software load balancers
NGINX
- Most popular web server / reverse proxy
- HTTP, TCP, UDP load balancing
- Open source (free) + NGINX Plus (commercial)
- Event-driven, high performance
- Great documentation and community
Config example:
upstream backend {
least_conn;
server 10.0.0.1:8080 weight=3;
server 10.0.0.2:8080;
server 10.0.0.3:8080 backup;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}HAProxy
- High-performance TCP/HTTP load balancer
- Battle-tested (GitHub, Reddit, Stack Overflow, Twitter)
- Very detailed metrics and logging
- Advanced health checking
Envoy
- Modern, cloud-native proxy
- Originally from Lyft, now CNCF
- Foundation of service meshes (Istio, AWS App Mesh)
- Native gRPC support
- Dynamic configuration via xDS API
- Best for: Kubernetes, service mesh, microservices
Traefik
- Kubernetes-native
- Auto-discovery of services
- Automatic Let's Encrypt certificates
- Best for: Kubernetes, Docker Swarm
Further Reading: Load Balancing#
- Docs
NGINX Load Balancing Guide: https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/
- Docs
AWS Elastic Load Balancing: https://docs.aws.amazon.com/elasticloadbalancing/
- Docs
HAProxy Documentation: https://www.haproxy.com/documentation/
- Book
Google SRE Book - Load Balancing: https://sre.google/sre-book/load-balancing-frontend/
- Docs
Envoy Proxy: https://www.envoyproxy.io/docs/
1.3 Domain Name System (DNS)#
What Is DNS?#
DNS (Domain Name System) is the internet's phone book. It translates human-readable domain names (like www.example.com) into IP addresses (like 93.184.216.34) that computers use to identify each other.
Without DNS, you'd need to memorize IP addresses for every website you visit. DNS makes the internet usable by humans.
Why DNS Matters for System Design#
DNS is much more than name resolution. In modern systems, DNS is used for:
- Load balancing: Distributing traffic across multiple servers
- Geographic routing: Directing users to the nearest datacenter
- Failover: Automatically switching to backup servers when primary fails
- Service discovery: Letting microservices find each other
- Traffic management: Canary deployments, blue-green deployments
Understanding DNS deeply is essential for designing globally distributed, highly available systems.
How DNS Works — Step by Step#
When you type www.example.com in your browser, here's what happens:
%% title: DNS resolution flow sequenceDiagram participant B as Browser participant OS as Operating system participant R as DNS resolver participant RT as Root server participant T as .com TLD server participant A as ns1.example.com Note over B: STEP 1 · Browser cache check B->>B: "Have I looked up this domain recently?" Note over B: If YES → use cached IP<br/>(typically cached for TTL duration)<br/>If NO → ask Operating System Note over B,OS: STEP 2 · Operating system cache B->>OS: Resolve www.example.com OS->>OS: "Is this domain in my local DNS cache?" Note over OS: If YES → return cached IP to browser<br/>If NO → ask configured DNS resolver Note over OS,R: STEP 3 · DNS resolver (recursive resolver) OS->>R: Ask configured DNS resolver<br/>(ISP or 8.8.8.8 or 1.1.1.1) R->>R: "Is it in my cache?" Note over R: If YES → return cached IP<br/>If NO → start recursive resolution (steps 4-6) Note over R,RT: STEP 4 · Root DNS server R->>RT: "Where is www.example.com?" Note over RT: 13 root server identities<br/>(a.root-servers.net through m.root-servers.net)<br/>~2,000+ instances worldwide via Anycast RT-->>R: "I don't know, but .com TLD is handled by:<br/>a.gtld-servers.net, b.gtld-servers.net, ..." Note over R,T: STEP 5 · TLD (Top Level Domain) server R->>T: "Where is www.example.com?" T-->>R: "example.com is handled by nameservers:<br/>ns1.example.com (IP 93.184.216.34)<br/>ns2.example.com (IP 93.184.216.35)" Note over R,A: STEP 6 · Authoritative DNS server R->>A: "What is IP of www.example.com?" A-->>R: "www.example.com = 93.184.216.34, TTL=300" Note over B,R: STEP 7 · Response returns R->>R: Cache answer (respects TTL) R-->>OS: Return IP OS->>OS: Cache OS-->>B: Return IP B->>B: Cache and connect to 93.184.216.34
Timing:
- Full resolution (no cache): 50-200ms
- Cached at resolver: 1-10ms
- Cached locally: <1ms
DNS Record Types#
A record (Address)
Maps domain to IPv4 address
example.com. 300 IN A 93.184.216.34
api.example.com. 60 IN A 10.0.1.100Multiple A records = DNS round-robin load balancing
api.example.com. 60 IN A 10.0.1.100
api.example.com. 60 IN A 10.0.1.101
api.example.com. 60 IN A 10.0.1.102AAAA record (IPv6)
Maps domain to IPv6 address
example.com. 300 IN AAAA 2606:2800:220:1:248:1893:25c8:1946CNAME record (Canonical Name / Alias)
Creates an alias pointing to another domain
www.example.com. 300 IN CNAME example.com.
cdn.example.com. 300 IN CNAME d123456.cloudfront.net.
blog.example.com. 300 IN CNAME mycompany.ghost.io.Important: Cannot use CNAME at zone apex (example.com). Workarounds: ALIAS (Route 53), ANAME, CNAME flattening (Cloudflare)
MX record (Mail Exchange)
Specifies mail servers for the domain
example.com. 300 IN MX 10 mail1.example.com.
example.com. 300 IN MX 20 mail2.example.com. (backup)The number after MX is the Priority (lower = higher priority).
TXT record (Text)
Stores text, used for verification and email security
; Domain verification:
example.com. IN TXT "google-site-verification=abc123..."
; SPF (email authentication):
example.com. IN TXT "v=spf1 include:_spf.google.com ~all"
; DMARC (email policy):
_dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=..."NS record (Name Server)
Delegates DNS authority to nameservers
example.com. 86400 IN NS ns1.example.com.
example.com. 86400 IN NS ns2.example.com.DNS in System Design#
1. Geographic routing (GeoDNS)
Route users to nearest datacenter:
| User | Query | Resolves to |
|---|---|---|
| User in Tokyo | api.example.com |
10.1.0.1 (Asia server) |
| User in NYC | api.example.com |
10.2.0.1 (US East server) |
| User in London | api.example.com |
10.3.0.1 (EU server) |
Benefits: Lower latency, data residency compliance
2. Failover
Automatic switch to backup when primary fails:
- Primary healthy:
api.example.com→primary-dc.example.com - Primary fails:
api.example.com→backup-dc.example.com
TTL affects failover speed! Lower TTL = faster failover
3. Weighted routing (Canary Deployments)
Split traffic between versions — api.example.com:
- Weight 95:
stable.internal(v1.0) - Weight 5:
canary.internal(v1.1)
4. Service discovery (Internal DNS)
Services find each other by DNS name:
user-service.internal→ 10.0.1.1, 10.0.1.2, 10.0.1.3order-service.internal→ 10.0.2.1, 10.0.2.2
No hardcoded IPs! Works with auto-scaling.
DNS Providers#
Managed DNS
- Route 53 (AWS) — Health checks, latency/geo routing, $0.50/zone/mo
- Cloudflare DNS — Free, fastest, DDoS protection, instant propagation
- Cloud DNS (GCP) — Anycast, private zones
- Azure DNS — Azure integration
Public resolvers
- 8.8.8.8 / 8.8.4.4 — Google Public DNS
- 1.1.1.1 / 1.0.0.1 — Cloudflare (fastest)
- 9.9.9.9 — Quad9 (security-focused)
- 208.67.222.222 — OpenDNS
Further Reading: DNS#
- Read
How DNS Works (Interactive Comic): https://howdns.works/
- Docs
AWS Route 53 Documentation: https://docs.aws.amazon.com/Route53/
- Read
Cloudflare DNS Learning: https://www.cloudflare.com/learning/dns/what-is-dns/
Edge layer: CDN, load balancer, DNS
Q1/4 You ship a fix to
app.js, which the CDN caches with a 1-year TTL, and users keep getting the old file. What is the recommended fix for static assets?Answer & explanation
Answer: C
With versioned URLs, new content means a new URL and an automatic cache miss: instant, no purge delay, safe rollbacks. A 60-second TTL works, but it throws away most of your hit ratio and sends far more requests to the origin.
Q2/4 A response has
Cache-Control: public, no-cacheplus an ETag. What does the CDN do with it?Answer & explanation
Answer: A
no-cache means "cache, but revalidate first"; if the ETag still matches, the origin answers 304 with no body. "Never store" is
no-store. Mixing the two up is a classic mistake.Q3/4 You need
/api/v2/*routed to a new target group and TLS terminated at the load balancer. Which load balancer do you need?Answer & explanation
Answer: B
Only L7 sees the decrypted HTTP request (path, host, headers), so only L7 can route on
/api/v2. L4 is faster, but it sees only IP and port; the payload is opaque to it.Q4/4 Your LB health check queries the database, Redis and an external API. The database has a 30-second blip. What happens?
Answer & explanation
Answer: B
A deep check fails on every server together when a shared dependency is down, so the LB marks the whole pool unhealthy. That is why the lesson uses a shallow
/healthcheck for the LB and keeps deep checks for monitoring.
Chapter 2: Application Layer#
The application layer is where your business logic runs.
2.1 Stateless vs Stateful Applications#
This is critical for scalability:
Stateful server (Problematic)
Server stores session data in memory:
- Request 1: User logs in → Server A stores session in memory
- Request 2: User loads profile → Must go to Server A (has session!)
- Request 3: Server A crashes → Session LOST! User logged out!
Problems
- User MUST return to same server (sticky sessions required)
- Server crash = data loss
- Can't add servers easily (new servers don't have sessions)
- Memory grows with users
- Horizontal scaling is painful
Stateless server (Preferred)
Server stores NO session data. All state is external.
- Request 1: User logs in → Server A creates session in Redis. Returns JWT token to client
- Request 2: User loads profile → Goes to Server B (any server works!). Server B validates JWT, gets session from Redis
- Request 3: Server A crashes → No problem! User continues on B, C
Benefits
- Any server can handle any request
- Easy horizontal scaling (just add servers)
- Server failure = no data loss
- Simple load balancing (round-robin works)
- Easy deployments (no session migration)
Where to store state
- Sessions → Redis (fast, TTL support)
- User data → PostgreSQL (persistent)
- Files → S3 (durable)
- Cache → Redis/Memcached
2.2 Monolith vs Microservices#
Monolith
Single application containing a User Module and an Order Module, over a shared database.
Pros
- Simple to develop
- Easy to test
- Simple deployment
- No network latency
- ACID transactions easy
Cons
- Scale everything together
- Single failure affects all
- Technology lock-in
- Slow builds as code grows
- Team coordination hard
Start with monolith when:
- Small team (< 10 developers)
- New product (unclear requirements)
- Need to move fast (MVP)
- Limited DevOps expertise
Microservices
User Service, Order Service, Payment Service — each with its own DB. Independent databases & deploys.
Pros
- Scale services independently
- Technology diversity
- Isolated failures
- Team autonomy
- Faster CI/CD per service
Cons
- Complex operations
- Network latency
- Distributed transactions hard
- Testing complexity
- Need observability tooling
Consider microservices when:
- Large team (can dedicate team per service)
- Clear service boundaries exist
- Different scaling requirements
- Strong DevOps culture
- Have observability infrastructure
"Start with a monolith, extract services when needed" (paraphrasing Martin Fowler, "MonolithFirst")
Further Reading: Architecture#
- Blog
Martin Fowler - Microservices: https://martinfowler.com/articles/microservices.html
- Read
Monolith First: https://martinfowler.com/bliki/MonolithFirst.html
- Book
Building Microservices (Book) - Sam Newman
- Read
Microservices.io Patterns: https://microservices.io/
Stateless apps and monolith vs microservices
Q1/2 Sessions live in each app server's memory. Server A crashes during a traffic peak. What do its users see, and what is the fix?
Answer & explanation
Answer: A
In-memory state dies with the process. Externalizing it (Redis for sessions) lets any server handle any request. Sticky sessions are the symptom, not the cure: they still lose the session when that one server dies.
Q2/2 A 6-person team is building a new product whose requirements are still unclear. Which architecture does the lesson recommend?
Answer & explanation
Answer: C
Small team, unclear requirements, need to move fast: all the "start with monolith" signals. Microservices pay off with large teams and clear boundaries; before that you pay the ops and distributed-transaction cost for nothing.
Chapter 3: Data Layer#
Moving money between two accounts
A transfer takes 100 from account 1 and adds 100 to account 2. The server could crash between the two writes. Which kind of database do you pick for the balances, and what feature are you relying on?
One strong answer
- A relational (SQL) database with an ACID transaction. Both updates go between
BEGINandCOMMIT, so both happen or neither does. - Without that, a crash between the writes makes 100 disappear (or appear from nowhere).
- The selection guide sends "Need ACID transactions?" to PostgreSQL or MySQL. The NoSQL stores are picked for flexible schemas, speed or write volume instead.
3.1 SQL vs NoSQL Databases#
SQL (Relational) databases
Examples: PostgreSQL, MySQL, SQL Server
Structure: Fixed schema, tables with rows and columns
| users | orders | ||
|---|---|---|---|
id (PK) |
INT | id (PK) |
INT |
email |
TEXT | user_id (FK) → users (1:N) |
INT |
name |
TEXT | total |
DECIMAL |
created_at |
TIMESTAMP | status |
TEXT |
ACID transactions:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- Both happen or neither happensBest for
- Complex queries with JOINs
- Transactions (banking, orders)
- Data integrity critical
- Structured, predictable data
- Reporting and analytics
Default choice: PostgreSQL (handles most use cases well)
NoSQL database types
1. Document store (MongoDB, CouchDB)
{
"_id": "user_123",
"name": "John",
"orders": [ // Embedded documents
{"id": "ord1", "total": 99.99}
]
}- ok: Flexible schema, natural for hierarchical data
- no: No JOINs (denormalize instead)
- Best for: Content management, catalogs, user profiles
2. Key-value store (Redis, DynamoDB)
-
user:123→{"name": "John", "email": "j@ex.com"} -
session:abc→{"user_id": 123, "expires": ...} -
ok: Extremely fast O(1) lookups
-
no: No complex queries
-
Best for: Sessions, caching, real-time data, leaderboards
3. Wide-column store (Cassandra, HBase)
| Row Key | Column1 | Column2 | Column3 |
|---|---|---|---|
user:123 |
name:John |
email:j@ex.com |
age:30 |
user:456 |
name:Jane |
city:NYC |
(no age!) |
- ok: Massive write throughput, linear scaling
- no: Limited query patterns
- Best for: Time-series, IoT, logging, high write volume
4. Graph database (Neo4j, Neptune)
-
(Alice)—FOLLOWS→(Bob)—WORKS_AT→(Acme) -
ok: Natural for connected data, fast traversals
-
Best for: Social networks, recommendations, fraud detection
Database selection quick guide
| Need | Use |
|---|---|
| Need ACID transactions? | PostgreSQL, MySQL |
| Flexible schema? | MongoDB, DynamoDB |
| Simple key-value + caching? | Redis |
| Massive writes? | Cassandra |
| Full-text search? | Elasticsearch |
| Graph relationships? | Neo4j |
| Default choice? | PostgreSQL |
3.2 Caching#
Why caching matters
Without cache: Every request → Database (50ms). 1000 requests = 1000 DB queries = DB overload
With cache (90% hit rate):
- 900 requests → Cache (1ms)
- 100 requests → Database (50ms)
- Average latency: (0.9 × 1) + (0.1 × 50) = 5.9ms
- vs 50ms without cache = 8.5x faster!
- Database load reduced by 90%!
Cache-aside pattern (Most Common)
def get_user(user_id):
# 1. Check cache first
cached = redis.get(f"user:{user_id}")
if cached:
return json.loads(cached) # Cache HIT
# 2. Cache MISS - query database
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
# 3. Store in cache for next time
redis.setex(f"user:{user_id}", 3600, json.dumps(user)) # 1hr TTL
return user
# INVALIDATION (on update):
def update_user(user_id, data):
db.update(user_id, data)
redis.delete(f"user:{user_id}") # Invalidate cacheCache eviction policies
| Policy | Behaviour |
|---|---|
| LRU (Least Recently Used) | Remove least recently accessed (most common choice; note Redis defaults to noeviction unless configured) |
| LFU (Least Frequently Used) | Remove least often accessed |
| FIFO (First In First Out) | Remove oldest |
| TTL (Time To Live) | Remove after expiration time |
Redis vs Memcached
- Redis: Rich data structures, persistence, pub/sub, Lua scripts
- Memcached: Simpler, multi-threaded, more memory efficient
Default choice: Redis (more versatile)
| Value | |
|---|---|
| Incoming requests (the load balancer example) | 50,000/s over 3 app servers → ~17,000/s each |
| After one server fails | 50,000 ÷ 2 = 25,000/s each |
| User reads per request (assumed) | 1 → 50,000 reads/s |
| Cache hit rate (assumed, as in the caching example) | 90% → 45,000/s from Redis, 5,000/s to the database |
| Average read latency (1 ms hit, 50 ms miss) | 0.9 × 1 + 0.1 × 50 = 5.9 ms |
| If the hit rate drops to 80% | 10,000/s to the database, 10.8 ms average |
Conclusion: size each app server for 25,000 requests a second, not 17,000, so the pool survives losing one. Size the database for well above 5,000 queries a second, because a small drop in the cache hit rate doubles its load.
Further Reading: Databases & Caching#
- Blog
PostgreSQL Documentation: https://www.postgresql.org/docs/
- Docs
Redis Documentation: https://redis.io/docs/
- Read
MongoDB University: https://university.mongodb.com/
- Book
Designing Data-Intensive Applications (Book) - Martin Kleppmann
3.3 Object Storage#
Structure
Bucket: my-app-uploads
images/profiles/user123.jpg(Object)videos/uploads/intro.mp4(Object)documents/reports/q3.pdf(Object)
Characteristics
Pros
- Virtually unlimited scale
- 99.999999999% durability (11 nines!)
- HTTP access (easy CDN integration)
- Cheap (compared to block storage)
- Versioning, lifecycle policies
Cons
- Not a native filesystem (FUSE tools like Mountpoint/s3fs exist, with limits)
- No partial updates (replace whole object)
- Higher latency than local disk
Use cases
- User uploads (images, videos, documents)
- Static website assets
- Backups and archives
- Data lake storage
Providers
- AWS S3 (the original, most popular)
- Google Cloud Storage
- Azure Blob Storage
- Cloudflare R2 (S3-compatible, no egress fees!)
Databases, caching and object storage
Q1/3 Cache hits take 1ms and database reads 50ms. At a 90% hit rate the average is about 5.9ms. What is the average if the hit rate falls to 80%?
Answer & explanation
Answer: A
0.8 x 1ms + 0.2 x 50ms = 0.8 + 10 = 10.8ms. Misses dominate, so losing 10 points of hit rate nearly doubles latency. It also doubles the share of traffic hitting the database (10% to 20%), which is often the bigger problem.
Q2/3 With cache-aside,
update_userwrites to the database and then deletes the cache key. What goes wrong if you skip the delete?Answer & explanation
Answer: B
The cache does not know the DB changed, so the stale entry is served until it expires. Deleting it forces the next read to reload fresh data. Caches never roll back database writes; staleness is silent, which is what makes it dangerous.
Q3/3 You must ingest millions of IoT sensor readings per second and query them by device and time. Which store fits best?
Answer & explanation
Answer: C
Wide-column stores are built for massive write throughput and linear scaling, the lesson's pick for time-series and IoT. PostgreSQL is the default for transactions and joins, not for this write volume.
Chapter 4: Messaging Layer#
4.1 Message Queues#
%% title: Message queue patterns
flowchart TD
subgraph PS["Publish-subscribe (fan-out)"]
direction TB
PUB@{ icon: "sd:server", label: "Publisher" } --> T@{ icon: "sd:stream", label: "Topic" }
T --> SA["Subscriber A"]:::core
T --> SB["Subscriber B"]:::core
T --> SC["Subscriber C"]:::core
end
subgraph P2P["Point-to-point (work queue)"]
direction TB
PA@{ icon: "sd:server", label: "Producer A" } --> Q@{ icon: "sd:queue", label: "Queue<br/>[3][2][1]" }
PB@{ icon: "sd:server", label: "Producer B" } --> Q
Q --> C1["Consumer 1"]:::core
Q --> C2["Consumer 2"]:::core
end
class PUB,PA,PB service
class T,Q queue- Point-to-point (work queue): each message consumed by ONE consumer only. Use for: background jobs, task distribution, work queues.
- Publish-subscribe (fan-out): each message delivered to ALL subscribers. Use for: events, notifications, real-time updates.
Message queue comparison:
| RabbitMQ | Kafka | SQS | Redis Streams | |
|---|---|---|---|---|
| Throughput | Medium | Very High | High | Medium |
| Replay | No (classic queues; Streams: yes) | Yes | No | Yes |
| Ordering | Queue | Partition | FIFO opt | Stream |
| Persistence | Optional | Yes | Yes | Optional |
| Best for | Routing | Events | Tasks | Lightweight |
When to use queues:
- ✓ Async processing (user doesn't wait for slow task)
- ✓ Decoupling services (Order → Inventory, Email, Analytics)
- ✓ Handling traffic spikes (queue buffers requests)
- ✓ Reliable delivery (payment events)
- ✓ Rate limiting external APIs
What breaks if email is down in a synchronous chain?
Order Service calls Inventory, then Email, then Analytics, waiting for each reply in turn. The Email service goes down for ten minutes. What happens to new orders? What changes if Order Service publishes an OrderCreated event instead?
One strong answer
- Synchronous chain: every order waits on Email and then fails. One broken service stops checkout, even though inventory and analytics are fine.
- Event bus: Order Service creates the order, emits one event and moves on. Inventory and Analytics consume it as usual. Email catches up from the bus when it's back.
- The price: the flow is harder to trace, and the confirmation email arrives late (eventual consistency).
4.2 Event-Driven Architecture#
Instead of: Order Service → calls → Inventory → calls → Email → calls → Analytics (synchronous chain - slow, coupled, one failure breaks all)
We do:
%% title: Event-driven architecture
flowchart TD
OS["Order Service<br/>1. Create order<br/>2. Emit event: OrderCreated"]:::core
OS --> BUS@{ icon: "tech:kafka", label: "Event bus (Kafka)<br/>{ #quot;type#quot;: #quot;order.created#quot;, #quot;order_id#quot;: #quot;123#quot;, #quot;total#quot;: 99.99 }" }
BUS -.-> INV@{ icon: "sd:warehouse", label: "Inventory Service<br/>Reserve stock" }
BUS -.-> EM@{ icon: "sd:mail", label: "Email Service<br/>Send confirm" }
BUS -.-> AN@{ icon: "sd:chart", label: "Analytics Service<br/>Track metrics" }
class BUS queue
class INV,EM,AN serviceBenefits:
- ✓ Services don't know about each other (loose coupling)
- ✓ Easy to add new consumers
- ✓ Failure isolation
- ✓ Event replay capability
Challenges:
- ✗ Harder to trace request flow
- ✗ Eventual consistency
- ✗ Need good observability
Further Reading: Messaging#
- Read
RabbitMQ Tutorials: https://www.rabbitmq.com/getstarted.html
- Docs
Kafka Documentation: https://kafka.apache.org/documentation/
- Docs
AWS SQS Guide: https://docs.aws.amazon.com/sqs/
- Read
Event-Driven Architecture (AWS): https://aws.amazon.com/event-driven-architecture/
Messaging and event-driven design
Q1/2 Each uploaded image needs exactly one worker to resize it. Which messaging pattern fits?
Answer & explanation
Answer: C
In a work queue each message is consumed by one consumer only, which is exactly job distribution. Pub-sub delivers to all subscribers, so every worker would resize the same image.
Q2/2 A new analytics service must consume
OrderCreatedevents and also reprocess last week's orders. Which option from the comparison supports that directly?Answer & explanation
Answer: A
Kafka keeps the event log, so a new consumer can replay old events. It also lets you add consumers without touching the producer. SQS and classic RabbitMQ queues remove a message once it is consumed, so last week's events are gone.
Chapter 5: Quick Reference#
Component Selection Cheatsheet#
| "I need to..." | Use... |
|---|---|
| Cache static content globally | CDN (CloudFront, Cloudflare) |
| Distribute traffic to servers | Load Balancer (ALB, Nginx) |
| Handle auth, rate limiting for APIs | API Gateway (Kong, AWS APIGW) |
| Route domain to servers | DNS (Route 53, Cloudflare) |
| Store structured data + transactions | PostgreSQL, MySQL |
| Store documents, flexible schema | MongoDB, DynamoDB |
| Fast key-value lookups, caching | Redis |
| Store files (images, videos) | S3, GCS |
| Full-text search | Elasticsearch, Algolia |
| Async task processing | SQS, RabbitMQ |
| High-throughput event streaming | Kafka |
| Real-time pub/sub | Redis Pub/Sub |
Default choices:
- Database: PostgreSQL
- Cache: Redis
- Queue: SQS (AWS) or Kafka (high throughput)
- CDN: CloudFront (AWS) or Cloudflare
- Object Storage: S3
Typical Architecture Patterns#
%% title: Typical architecture patterns
flowchart TD
subgraph RT["Real-time app"]
direction TB
c1["CDN"]:::edge --> c2["Load Balancer"]:::edge --> c3["WebSocket Servers"]:::core --> c4@{ icon: "tech:redis", label: "Redis Pub/Sub" } --> c5@{ icon: "tech:kafka", label: "Kafka" }
end
subgraph MS["Microservices"]
direction TB
b1["CDN"]:::edge --> b2["API Gateway"]:::edge --> b3["Load Balancers"]:::edge --> b4["Services"]:::core
b4 --> b5@{ icon: "sd:database", label: "DBs" }
b4 --> b6@{ icon: "tech:kafka", label: "Kafka" }
end
subgraph SW["Simple web app"]
direction TB
a1["CDN"]:::edge --> a2["Load Balancer"]:::edge --> a3["App Servers"]:::core
a3 --> a4@{ icon: "tech:postgresql", label: "PostgreSQL" }
a3 --> a5@{ icon: "tech:redis", label: "Redis" }
end
class c4,a5 cache
class c5,b6 queue
class b5,a4 storeSummary#
Edge layer
- CDN: Cache static content at edge, 95%+ hit ratio target
- Load Balancer: Distribute traffic, L4 vs L7, health checks
- API Gateway: Auth, rate limiting, routing, transformation
- DNS: Domain resolution, geo routing, failover
Application layer
- Keep servers STATELESS (externalize state to Redis/DB)
- Start MONOLITH, extract microservices when needed
Data layer
- SQL (PostgreSQL): Transactions, relationships, complex queries
- NoSQL: Document (MongoDB), Key-Value (Redis), Column (Cassandra)
- Caching: Cache-aside pattern, LRU eviction
- Object Storage: S3 for files
Messaging layer
- Queues: Async processing, decoupling, traffic buffering
- Kafka: High-throughput event streaming
- Event-driven: Loose coupling, failure isolation
Comprehensive Resource List#
Books#
- Designing Data-Intensive Applications - Martin Kleppmann (Essential!)
- Building Microservices - Sam Newman
- System Design Interview - Alex Xu
- Web Scalability for Startup Engineers - Artur Ejsmont
Online Resources#
- High Scalability Blog: http://highscalability.com/
- AWS Architecture Center: https://aws.amazon.com/architecture/
- System Design Primer: https://github.com/donnemartin/system-design-primer
- ByteByteGo Newsletter: https://bytebytego.com/
Documentation#
- AWS Well-Architected: https://aws.amazon.com/architecture/well-architected/
- Google Cloud Architecture: https://cloud.google.com/architecture
- Azure Architecture Center: https://docs.microsoft.com/en-us/azure/architecture/
this lesson in shorts · 2
play them all →One idea from this lesson in about a minute each, made for your phone.
Go deeper · reading trail
all reading →Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.
Load Balancing at the Frontend
Piotr Lewandowski, Sarah Chavis · 2016- What
- A chapter of Google's Site Reliability Engineering book on how user traffic reaches the right datacenter.
- Key idea
- DNS load balancing picks a region but caches and resolvers blur it, so a virtual IP and network load balancer spread traffic inside the datacenter.
- Why here
- It shows DNS and the load balancer, two building blocks from this part, working as one layered traffic-routing system.
Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity
Kirsten Westeinde · 2019- What
- Shopify explains why it kept its Rails monolith and restructured it instead of splitting into microservices.
- Key idea
- A modular monolith with enforced component boundaries keeps one deployable while getting much of the isolation teams want from services.
- Why here
- A real counterweight to 'microservices by default' in the monolith vs microservices section: the boundary matters more than the network hop.
Goodbye Microservices: From 100s of problem children to 1 superstar
Alexandra Noonan · 2018- What
- Segment's story of splitting its event-delivery pipeline into one service per destination and then merging them back.
- Key idea
- Per-destination services multiplied queues, shared-library drift and on-call load, and one well-tested service turned out simpler and faster.
- Why here
- Shows the operational cost side of the microservices trade-off taught here, with the numbers that pushed a team back to a monolith.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

