Distributed Systems & Scalability
A Complete 10-Chapter Software Development Course
Table of Contents
- Why Systems Need to Scale
- Load Balancing
- Caching Strategies
- Database Scaling
- The CAP Theorem & Consistency Models
- Message Queues & Asynchronous Processing
- Rate Limiting & Throttling
- API Gateways & Service Discovery
- Fault Tolerance & Resilience Patterns
- Capstone — Designing a System at Scale
Why Systems Need to Scale
Distributed Systems & Scalability
Chapter 1 · Why Systems Need to Scale
Software Architecture Fundamentals answered "how should this system's own code be organized?" This course answers a genuinely different question: once that system is correctly organized, what happens when real load hits it? A correctly-layered, correctly-bounded architecture (Software Architecture Fundamentals' own OrderService/OrderRepository/PricingEngine) doesn't automatically survive scale — this chapter measures two completely different reasons why.
Reason 1: An Algorithmic Bottleneck, Hiding in Correct-Looking Code
A duplicate-order-ID check is a natural thing to add to OrderRepository. The obvious implementation scans every existing order — and that "obvious" choice is the actual bottleneck.
Reason 2: A Single Process Can Only Use So Much Hardware
Even a perfectly-written PricingEngine runs as ordinary Python code in one process. Genuinely parallelizing it — running several calculations at once, on several CPU cores — needs more than just "a bigger server."
multiprocessing.Pool) took 0.207s — roughly 3× slower, not faster. Spinning up separate OS processes has real overhead, and for a workload this small, that overhead cost more than the parallelism saved.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A 1,785× algorithmic slowdown, invisible until measured at real scale | Chapter 4's Database Scaling — sharding a growing dataset is the production-scale version of fixing exactly this kind of bottleneck |
| Small-workload parallelism verified genuinely slower, not just "less beneficial" | Chapter 2's Load Balancing — routing overhead has the same shape of cost, worth measuring before assuming it's free |
| Sub-linear speedup even for a large, genuinely parallelizable workload | Technical Support's own `perfdiag1` — this chapter's own honest ceiling is exactly the kind of number that course's diagnostic chapters teach how to notice in a real production dashboard |
Hands-On Exercises
Repeat this chapter's own linear-vs-set benchmark, but check for an order ID that's first in the list rather than last. Verify the linear check's timing changes dramatically while the set check's doesn't, and explain why using this chapter's own O(n)/O(1) framing.
📄 View solutionRe-run this chapter's own small-workload multiprocessing benchmark (8 calculations) with only 2 processes instead of 4. Verify whether the "parallelizing makes it slower" finding still holds, and report the actual speedup ratio.
📄 View solutionUsing this chapter's own two verified findings, explain why "just add more servers" is not a universal fix for a slow system — name the specific condition each finding shows has to be true before horizontal scaling actually helps.
📄 View solutionChapter 1 Quick Reference
- Vertical scaling: a more powerful single machine — helps uniformly, but has a ceiling and never fixes an algorithmic growth-rate problem
- Horizontal scaling: more machines/processes working in parallel — verified: genuinely slower for a small workload (overhead dominates), genuinely faster but sub-linear for a large one (1.63×–2.81× across 2–8 processes)
- Verified: a linear duplicate-check grew from 104× to 1,785× slower than an O(1) equivalent as data size grew from 1,000 to 20,000 — a bottleneck no amount of scaling alone fixes
- Next chapter: Load Balancing — how requests actually get distributed once there's more than one server to send them to
Load Balancing
Distributed Systems & Scalability
Chapter 2 · Load Balancing
Chapter 1 showed horizontal scaling only helps once work can genuinely be split across machines. Load balancing is the part that actually does the splitting — and how it splits requests turns out to matter as much as whether it splits them at all.
Round Robin vs. Least Connections, With Unequal Servers
Three servers — two fast (capacity 5 requests/tick), one genuinely slow (capacity 2/tick) — under 12 new requests every tick for 20 ticks:
Health Checks: Routing Around a Server That's Actually Down
B going down partway through 30 requests: a plain Round Robin balancer with no health checking kept routing roughly a third of all remaining requests to the dead server, producing 7 failed requests out of 30. The identical scenario, routed through a balancer that filters to only currently-healthy servers before choosing one, produced 0 failed requests — every request was automatically redirected to A or C instead.
healthy boolean, checked with a plain if. Design Patterns Chapter 8 modeled a genuinely richer set of behaviors — an order's status — as full state objects, each owning its own transition rules. If a real health-check system needed more than "route or don't" (say, a "draining" state that finishes existing connections but accepts no new ones), the boolean would stop being enough, and reaching for that same State pattern would be the natural next step — not a different idea, just a heavier tool for a genuinely more complex version of the same problem.
Sticky Sessions: Solving Statelessness's Problem, Creating a New One
Software Architecture Fundamentals Chapter 8 verified a stateful design losing data on a server restart, and a stateless one surviving it. Sticky sessions are the load-balancer-level alternative: pin a client to the same server for their whole session, so that server can hold state in memory safely. What does pinning cost?
0.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| An unbounded queue under Round Robin with unequal server speeds | Chapter 1's own horizontal-scaling findings — unequal processing capacity is exactly the condition that makes a "fair share of requests" different from "fair share of work" |
| Zero failures with health checking vs. 7 without | Chapter 9's own Fault Tolerance & Resilience Patterns — health checking is the first, simplest resilience pattern this course covers |
| Sticky sessions' verified 40-request spread from pure hash luck | Software Architecture Fundamentals Chapter 8's own statelessness finding — the two chapters verify opposite sides of the identical tradeoff |
Hands-On Exercises
Re-run this chapter's own Round Robin vs. Least Connections simulation with two slow servers (capacity 2/tick) and only one fast server (capacity 5/tick). Verify whether Least Connections still keeps queues bounded, and report the new queue depths for both balancers.
📄 View solutionUsing this chapter's own health-checking simulation, make two of the three servers go down at different points (B at request 10, C at request 20). Verify the health-checking balancer still produces zero failures, and report how many requests land on the one server left standing.
📄 View solutionUsing this chapter's own two verified findings (unequal-speed Round Robin, and sticky sessions), explain what specifically these two scenarios have in common: why does "give every option an equal share" produce a worse outcome than "route based on current state" in both cases?
📄 View solutionChapter 2 Quick Reference
- Round Robin: equal share of requests — verified: let a slow server's queue grow to
40and keep climbing, when server speeds genuinely differ - Least Connections: routes to whichever server has the least outstanding work — verified: kept the same slow server's queue capped at
3 - Health checks, verified:
7failed requests without them,0with them, for the identical mid-run server failure - Sticky sessions, verified: a
40-request spread from pure hash luck across 3 identical servers, versus0spread for the same traffic under Least Connections — the direct tradeoff against Software Architecture Fundamentals Chapter 8's own statelessness finding - Next chapter: Caching Strategies — cache-aside, write-through, write-behind, and why invalidation is the genuinely hard part
Caching Strategies
Distributed Systems & Scalability
Chapter 3 · Caching Strategies
Caching trades correctness risk for speed — every strategy in this chapter is a different way of drawing that trade. The famous line about cache invalidation being one of the two hard problems in computer science isn't a joke about difficulty in the abstract; this chapter builds a real, verified case of exactly what goes wrong.
Cache-Aside: Check the Cache First, Populate on Miss
PROD-1 (a genuine cache miss) took 10.37ms and incremented db.query_count to 1. Twenty subsequent reads of the identical key averaged 0.0005ms each — db.query_count never moved past 1. Cache-hit reads were roughly 19,566× faster than the original cache-miss read.
Write-Through vs. Write-Behind: Where the Real Tradeoff Lives
flush() ever runs, took 0.02ms total — roughly 12,145× faster, because nothing has touched the database yet.
write_behind.set('PROD-1', 100) and then checking database.get('PROD-1') before flush() runs returns None — the write genuinely never reached the database. cache.cache['PROD-1'] correctly shows 100 the whole time. Simulating a crash at exactly this point (the process ends, flush() never gets called) confirms the write is gone — not delayed, gone. Only once flush() actually runs does database.get('PROD-1') correctly return 100.
set() returns, the database and cache genuinely agree. Write-behind defers that cost — verified 12,145× faster — but during the deferral window, the only copy of the truth lives in memory, verified vanishing entirely on a simulated crash. This is the identical shape of tradeoff Distributed Systems & Scalability's own future CAP Theorem chapter formalizes: speed and immediate durability aren't both free at the same time.
Cache Invalidation: The Genuinely Hard Part
Reusing Software Architecture Fundamentals' own PricingEngine (Ch.7): what happens when a cached price outlives the data it was calculated from?
PROD-1 well-stocked (50 units), CachedPricingService.get_price() correctly caches and returns 100 (no scarcity pricing). Stock then genuinely drops to 5 — a real change that should trigger scarcity pricing. Querying the identical cache key again, without invalidating anything, still returns the stale 100 — while the correct price, computed fresh, is now 115.0. The customer would be quoted the wrong price, with no error, no warning, and nothing in the code path that looks broken.
cached_service.invalidate('PROD-1') — clearing every cached entry for that product — and querying again correctly returns 115.0, matching the true current price exactly.
CDN Basics
A Content Delivery Network applies cache-aside's own idea geographically: static content (images, scripts, stylesheets) gets cached at servers physically close to each visitor, instead of every request crossing however much distance separates the visitor from the origin server. Software Architecture Fundamentals Chapter 4 measured a real, non-zero cost for crossing a network boundary even on localhost — a CDN exists specifically to shrink that same kind of cost when the distance is a real geographic one, not a loopback address.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Write-behind's verified 12,145× speed gain, traded for a verified real data-loss window | Chapter 5's own CAP Theorem & Consistency Models — the same speed-vs-guarantee trade, formalized |
| A real, verified stale-price bug from a missing invalidation trigger | Software Architecture Fundamentals Chapter 6's own eventual-consistency finding — both are the same underlying risk (data that's technically present but no longer true) surfacing in different layers |
| Cache-aside's own 19,566× read speedup | Software Architecture Fundamentals Chapter 7's own ~18,111× testability speedup — a strikingly similar-shaped number, from the identical underlying idea: avoid real, slow I/O whenever it's safe to |
Hands-On Exercises
Extend this chapter's own CacheAsideRepository with a second key, 'PROD-2', and verify that a cache hit on 'PROD-1' doesn't accidentally also count as a hit for 'PROD-2' — confirm the first read of 'PROD-2' still triggers a genuine database query even after 'PROD-1' is fully cached.
Using this chapter's own WriteBehindCache, write two values for two different keys before ever calling flush(), then simulate a crash. Verify both writes are lost from the database, and verify calling flush() afterward (simulating a recovery that never happened) can't bring back data that was never in pending_writes to begin with.
This chapter's own CachedPricingService.invalidate() requires something else in the system to remember to call it whenever stock changes. Using this chapter's own verified stale-price bug, explain specifically what would have to change elsewhere in a real system (not in the cache itself) to make that invalidation call actually happen reliably.
Chapter 3 Quick Reference
- Cache-aside: check cache, miss falls through to the database and populates the cache — verified: ~19,566× faster on repeated reads, with
db.query_countnever incrementing past the first miss - Write-through: writes hit cache and database together, synchronously — always consistent, always pays the full write cost
- Write-behind: writes hit cache immediately, database later — verified ~12,145× faster, verified genuinely losing data on a simulated crash before
flush() - Invalidation, verified as the hard part: a real, wrong price (
100instead of the correct115.0) was served with zero errors until the cache was explicitly told to forget — the caching mechanism itself was never the problem - Next chapter: Database Scaling — replication, read replicas, and sharding
Database Scaling
Distributed Systems & Scalability
Chapter 4 · Database Scaling
A database can scale reads by copying itself (replication) or scale everything by splitting itself (sharding). Both genuinely work — and both cost something specific and measurable, not just "some consistency" in the abstract.
Read Replicas: Real Load Relief, and a Real Staleness Window
{'price': 100} to the primary makes it immediately available from primary.data. Reading the identical key from the replica before replicate_to() runs returns None — genuinely missing, not just slow. Only after replicate_to() actually runs does the replica correctly return {'price': 100}.
Sharding: Splitting Data, and What a Bad Key Does to It
user_id % 4 produced exactly 250 records per shard — a spread of 0 between the busiest and quietest shard.
0 on each of the remaining two. One shard alone carried 86.3% of all data. With only 4 distinct country values feeding the hash function, two of the four available shards never received a single record at all — sharding infrastructure was fully deployed, but most of it was doing nothing.
The Cost Sharding Adds: Cross-Shard Queries
100,000, matching a manual check (1,000 × $100) exactly. A question that was one query against an unsharded database becomes N queries plus a combine step, where N is however many shards exist.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A verified replica staleness window | Chapter 3's own cache-invalidation bug and Software Architecture Fundamentals Chapter 6's own eventual-consistency finding — the identical shape of risk, at the database layer this time |
| A dramatic, realistic sharding hotspot (86.3% of data on one shard) | Chapter 2's own Round Robin finding — an "equal treatment" rule (hash the key, mod by shard count) producing a badly uneven outcome |
| Cross-shard queries costing N shard-touches instead of 1 | Chapter 5's own CAP Theorem chapter — sharding is a concrete instance of trading single-query simplicity for horizontal capacity |
Hands-On Exercises
Using this chapter's own PrimaryDatabase/ReadReplica, write TWO keys to the primary before calling replicate_to(). Verify both are missing from the replica beforehand, and both are correctly present afterward.
Using this chapter's own sharding setup, try shard_by_country with a LESS skewed distribution — say, 30/25/25/20 percent across the same 4 countries instead of 80/7/7/6. Verify whether the hotspot shrinks, and report the new spread compared to this chapter's own 726-record spread.
Using this chapter's own two verified findings (replica staleness and the sharding hotspot), explain why "add read replicas" and "shard the database" solve genuinely different scaling problems — which one would have helped Chapter 1's own O(n) duplicate-check bottleneck, and why?
📄 View solutionChapter 4 Quick Reference
- Read replicas: verified reducing per-server load 3× (30 reads → 10 per server across 3 replicas) with zero change to any single query's own speed, at the cost of a real, verified staleness window before replication runs
- Sharding, verified: a good key (user_id) spread 1,000 records perfectly evenly; a plausible but poor key (signup country) put 86.3% of records on one shard and left two of four shards completely empty
- Cross-shard queries, verified: an aggregate across all data cost 4× the shard-touches of a single-shard query
- Next chapter: The CAP Theorem & Consistency Models — formalizing the exact tradeoffs this chapter and Chapter 3 both verified concretely
The CAP Theorem & Consistency Models
Distributed Systems & Scalability
Chapter 5 · The CAP Theorem & Consistency Models
"Pick two of Consistency, Availability, and Partition tolerance" is the version of CAP most people hear — and it's misleading. Partition tolerance isn't optional in a real distributed system; network partitions happen whether you design for them or not. The actual theorem is narrower and sharper: when a partition is actually happening, a system must choose between Consistency and Availability — and, this chapter verifies, that choice doesn't matter at all until a partition actually occurs.
No Partition: Both Choices Look Identical
stock=50 to either CPSystem or APSystem while partitioned is False produces the identical result both times: node_a.data == node_b.data is True for both. No request was ever rejected in either system. There is no observable difference between "a system designed to prioritize consistency" and "a system designed to prioritize availability" — until something actually goes wrong.
A Real Partition: The Tradeoff Becomes Real
partitioned = True and attempting to write stock=30 to node_b (the isolated node) through CPSystem correctly raises ConnectionError: Write rejected: partitioned, cannot guarantee consistency. node_b.data stays exactly as it was — no write happened, no risk of disagreement, and a real customer request genuinely failed.
APSystem: writing stock=45 to node_a (representing a real sale processed on that side) and, separately, stock=42 to node_b (a different real sale, processed on the isolated side) both succeed. node_a.data now shows {'stock': 45}; node_b.data shows {'stock': 42} — genuinely disagreeing, confirmed directly (node_a.data != node_b.data → True).
The Cost of Reconciling an AP System's Own Conflict
Once the partition heals, something has to decide which of the two conflicting values is "correct." A common, simple strategy: last-write-wins, by timestamp.
node_b's write (stock=42) happened microseconds after node_a's (stock=45), so last-write-wins correctly picks node_b's value — the reconciled stock becomes 42. But both writes represented real sales that genuinely happened: if both should have counted, the true combined stock is 50 − 5 − 8 = 37. The reconciled value (42) is off from the true combined value (37) by a real, unrecoverable 5 units — node_a's own sale wasn't merged with node_b's, it was silently overwritten and lost.
Strong vs. Eventual Consistency, Named Precisely
| Model | Guarantee | Already verified in this course as |
|---|---|---|
| Strong consistency | Every read reflects the most recent write, always | CPSystem's own behavior — verified here rejecting availability to keep this guarantee |
| Eventual consistency | Reads may be temporarily stale, but converge given enough time | Chapter 3's cache-invalidation bug; Chapter 4's replica staleness window; Software Architecture Fundamentals Chapter 6's own event-processing gap |
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| No observable difference between CP and AP without a partition | Chapter 9's own resilience-pattern chapter — designing for a partition that hasn't happened yet is exactly the discipline that chapter covers |
| AP's own verified information-loss on reconciliation | Chapter 4's own sharding chapter — a cross-shard write conflict is a structurally identical problem, one layer over |
| Write-behind's own verified crash-loss finding, revisited here as a genuine AP-style tradeoff | Chapter 3's own caching chapter — write-behind is, in CAP terms, an availability-favoring choice made at the cache layer specifically |
Hands-On Exercises
Using this chapter's own CPSystem, verify that writes to node_a (the non-isolated node) still succeed correctly during a partition, even though writes to node_b are rejected. Explain what this confirms about which specific node a CP system sacrifices availability for.
Using this chapter's own reconciliation scenario, implement a different strategy — "highest value wins" instead of last-write-wins — and verify what final stock value and what discrepancy from the true combined total (37) it produces.
📄 View solutionUsing this chapter's own verified findings, explain why a real production system's own choice of CP or AP would most plausibly be decided per-operation (some writes CP, others AP) rather than as one single, system-wide setting — use this chapter's own stock-tracking scenario alongside a login/authentication scenario to make the contrast concrete.
📄 View solutionChapter 5 Quick Reference
- CAP, precisely: the Consistency-vs-Availability tradeoff only exists during an actual network partition — verified: CP and AP behaved identically with no partition, both perfectly consistent and perfectly available
- During a partition, verified: CP rejected a write outright (unavailable, still consistent); AP accepted two conflicting writes (available, now genuinely inconsistent —
45vs.42) - Reconciliation cost, verified: last-write-wins produced
42against a true combined value of37— a real, permanent 5-unit loss of information, not just a delay - Next chapter: Message Queues & Asynchronous Processing — the mechanism most real systems actually use to manage exactly this kind of tradeoff deliberately
Message Queues & Asynchronous Processing
Distributed Systems & Scalability
Chapter 6 · Message Queues & Asynchronous Processing
Software Architecture Fundamentals Chapter 6 built an EventBus that dispatched to subscribers the instant publish() was called — decoupling who knows about whom, but not when the work happens. A message queue decouples both. This chapter verifies what that second kind of decoupling actually buys, and what it costs.
Real Buffering: Decoupling Timing, Not Just Knowledge
consume_all() anywhere yet leaves all 5 correctly sitting in queue.messages, genuinely unprocessed. Starting the consumer afterward correctly processes all 5, in the exact order they were published, and drains the queue to empty. Compare this to Software Architecture Fundamentals' own EventBus.publish(), which had no way to "publish" without a subscriber already attached to react immediately — this queue lets production and consumption happen on completely independent schedules.
At-Least-Once Delivery: Why Idempotency Isn't Optional
A message that's processed but never acknowledged — because the consumer crashed in between — gets redelivered. What happens depends entirely on whether the handler can safely run twice.
10 points, then simulating a crash after processing but before acknowledging it: the handler already ran once (points = 10), but the message stays in the queue since it was never acked. Redelivery correctly re-triggers the handler — and points becomes 20, not the correct 10. The same real award was granted twice for one real event.
processed_ids set before doing anything: points correctly stays at 10 after both the crashed first attempt and the successful redelivery. The message was still delivered twice — at-least-once delivery didn't change — but the handler's own idempotency absorbed the duplicate safely.
20-instead-of-10 bug just verified is not a queue malfunction; the queue did exactly what "at-least-once" means. The correctness burden sits entirely with the consumer.
Dead-Letter Queues: What Happens When a Message Can't Be Processed
max_retries=3: the first two attempts return 'requeued', the third returns 'dead-lettered' — the message moves into dead_letter_queue, carrying the actual error message ('malformed payload: missing required field'), and stops consuming further processing attempts entirely.
['requeued', 'requeued', 'success'] — dead_letter_queue stayed empty. The retry mechanism gave the same message multiple genuine chances, and a transient problem (the kind Chapter 9's own resilience-pattern chapter covers in depth) resolved itself without ever needing manual intervention.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
Genuine timing decoupling, verified against Software Architecture Fundamentals' synchronous EventBus | Software Architecture Fundamentals Chapter 6's own eventual-consistency window — a message queue is the deliberate, controlled version of that same gap |
| A real, verified double-processing bug from non-idempotent handling | Chapter 5's own AP reconciliation finding — both are "the same real event counted twice," at different layers |
| Dead-lettering only after genuine retries, verified not triggering on a recoverable transient failure | Chapter 9's own Fault Tolerance & Resilience Patterns — retry-with-backoff and dead-letter queues are close cousins, covered together there |
Hands-On Exercises
Using this chapter's own MessageQueue, publish 3 messages, consume just 1 of them (call consume_all won't work here — write a small loop that pops and handles only 1), then publish 2 more messages before finally consuming the rest. Verify the final processing order is correct.
Using this chapter's own AtLeastOnceQueue and idempotent handler pattern, simulate the message crashing twice before finally being acknowledged successfully (three total delivery attempts). Verify points still correctly ends at 10, not 30.
Using this chapter's own two verified findings (the double-processing bug and the dead-letter queue), explain why a message ending up in the dead-letter queue is not a case at-least-once delivery's own idempotency requirement can help with — what's the actual difference in what's failing?
📄 View solutionChapter 6 Quick Reference
- Message queues: buffer messages between producer and consumer — verified: 5 messages queued and stayed unprocessed with zero consumers running, then processed correctly, in order, once one started
- At-least-once, verified: a non-idempotent handler double-counted a redelivered message (
10 → 20); an idempotent one, tracking processed IDs, correctly stayed at10 - Dead-letter queues, verified: an always-failing message was retried twice then moved aside on the third attempt; a transient failure recovered on its own within the same retry budget and was never dead-lettered
- Next chapter: Rate Limiting & Throttling — protecting a system's own downstream services from being overwhelmed in the first place
Rate Limiting & Throttling
Distributed Systems & Scalability
Chapter 7 · Rate Limiting & Throttling
Chapter 1 measured what happens when load exceeds what a system can handle. Rate limiting is the deliberate decision to reject some requests before that happens, protecting whatever sits downstream. Three algorithms answer "how do I count requests fairly" in genuinely different ways — and one classic implementation has a real, verified bug most people don't expect.
Token Bucket: Allows a Burst, Then Throttles
10 (refilling at 2/sec): 11 requests fired instantly all return the exact expected pattern — the first 10 all succeed, and the 11th is correctly rejected, since the bucket started full and every request drains one token. Waiting 0.6s (refilling roughly 1.2 tokens) makes the next request correctly succeed again.
Leaky Bucket: Smooths a Burst Into a Steady Output Rate
leak() fifteen times released every accepted request in the exact order it was originally submitted — ['req-0', 'req-1', ..., 'req-14'], confirmed matching the submission order exactly. Downstream code calling leak() only ever sees one request at a time, however bursty the input was.
leak() at a time. Choose token bucket when occasional bursts are genuinely fine; choose leaky bucket when downstream specifically needs a smooth, predictable rate — for instance, exactly the kind of steady processing rate a queue consumer (Chapter 6) or a database write path (Chapter 4) benefits from.
Fixed Window vs. Sliding Window: A Real Boundary Bug
A fixed window counts requests within calendar-aligned buckets (e.g., minute 0–60, 60–120) and resets the count at each boundary. What happens to traffic that straddles that boundary?
t=55 and t=59.5 (the tail end of one window) correctly allows all 10. Sending 10 more requests between t=60.5 and t=65 — barely 5.5 to 10.5 seconds later, but now in the "next" window — also allows all 10. Total: 20 requests genuinely allowed within a single 10-second span, against a limit meant to cap traffic at 10 per full 60 seconds.
0 of 10 allowed. Total allowed across the identical 10-second span: 10, matching the intended limit exactly.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A verified, doubled-rate fixed-window bug | Chapter 1's own O(n) bottleneck finding — both are bugs invisible in a simple test, only surfacing under a specific, real traffic shape |
| Leaky bucket's own steady, one-at-a-time release rate | Chapter 6's own message queue — leak() is structurally the same operation as a queue consumer processing one message at a time |
| Rate limiting protecting a downstream system before it's overwhelmed | Chapter 9's own Fault Tolerance & Resilience Patterns — rate limiting is a preventative resilience pattern, applied before a failure rather than reacting to one |
Hands-On Exercises
Using this chapter's own TokenBucket, verify what happens with a capacity of 5 and a much higher refill rate (10/sec) — send 5 immediate requests, then wait 0.3s and send 5 more. Report how many of the second batch succeed and explain why using this chapter's own refill formula.
Using this chapter's own LeakyBucket, submit 10 requests, leak out 3, then submit 8 more before leaking the rest. Verify the bucket correctly rejects any requests that would exceed its own capacity at the moment they're submitted, and verify the final leak order is still fully FIFO across both submission batches.
Using this chapter's own fixed-window and sliding-window results, construct a traffic pattern that does not straddle a window boundary (e.g., all 20 requests sent between t=10 and t=20, well inside one window). Verify both limiters now agree on how many requests are allowed, and explain why the boundary bug specifically requires boundary-straddling traffic to appear at all.
Chapter 7 Quick Reference
- Token bucket: allows a burst up to capacity, then throttles — verified: exactly 10 of 11 immediate requests succeeded against a 10-token bucket
- Leaky bucket: queues a burst, releases it downstream at a steady rate — verified: 15 of 20 burst requests accepted, released one at a time in exact FIFO order
- Fixed window, verified buggy: let 20 requests through in a 10-second span against a 10-per-60-second limit, purely from boundary timing
- Sliding window, verified correct: capped the identical traffic at exactly 10 in the same 10-second span
- Next chapter: API Gateways & Service Discovery — where rate limiting and routing typically get enforced in a real microservices system
API Gateways & Service Discovery
Distributed Systems & Scalability
Chapter 8 · API Gateways & Service Discovery
Software Architecture Fundamentals Chapter 4 measured a real, non-zero cost for every network call. This chapter shows the two standard fixes for a microservices system that's split into enough pieces for that cost to actually hurt: an API Gateway that reduces how many round trips a client pays for, and a service registry that lets services find each other without hardcoding addresses that will eventually go stale.
API Gateway: Aggregating Three Calls Into One Round Trip
50ms per round trip) calling three services directly — order, inventory, reviews — pays that 50ms cost three separate times: 166.9ms total. The identical client, calling one ApiGateway endpoint instead, pays the 50ms external cost once, with the gateway making the same three calls internally over a fast internal network (5ms each): 66.0ms total — 2.53× faster. Both approaches return identical data.
Service Discovery: Finding a Service That Doesn't Stay in One Place
InventoryService registers at 10.0.0.5:8080. A hardcoded client captures that address once, at startup. The service then genuinely redeploys — a real scaling or restart event — and re-registers at a new address, 10.0.0.9:8080; only that new address is actually listening now. The hardcoded client's own call, using its stale captured address, correctly returns False — it would silently fail against a service that no longer exists there. A client using service discovery — looking up the current address fresh, at the moment of the call — correctly finds 10.0.0.9:8080 and returns True.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A verified 2.53× client-side speedup from aggregating three calls into one | Software Architecture Fundamentals Chapter 4's own ~11,661× network-call overhead finding — this chapter's gateway is a direct, concrete application of minimizing exactly that cost |
| A hardcoded address silently breaking after a real redeploy | Chapter 2's own health-checking finding — both are examples of a system correctly routing around a change, versus one that doesn't notice at all |
| Service discovery as the missing piece behind horizontal scaling | Chapter 1's own scaling findings — this chapter closes the gap between "you can add more instances" and "the rest of the system can actually find them" |
Hands-On Exercises
Add a fourth service, ShippingService, to this chapter's own ApiGateway (with the same simulated 5ms internal latency), and measure the new total time with and without the gateway. Verify the gap between the two approaches widens as more services are aggregated.
Using this chapter's own ServiceRegistry, simulate a second redeploy (a third address) happening after the first. Verify a discovery-based client still correctly finds the newest address, and verify the hardcoded client (still holding its original, very first captured address) remains broken exactly as before.
Using this chapter's own two verified findings, explain why an API gateway that fans out to three services using hardcoded addresses would eventually break in the exact way this chapter's own hardcoded client did — and why a real gateway needs service discovery internally, not just aggregation.
📄 View solutionChapter 8 Quick Reference
- API Gateway: aggregates multiple backend calls into one client-facing round trip — verified:
2.53×faster from the client's own perspective (166.9ms→66.0ms), identical results - Service registry: lets services be found by name instead of hardcoded address — verified: a hardcoded client silently broke after a real redeploy; a discovery-based client correctly kept working
- The connection to Chapter 1: service discovery is what makes horizontal scaling's own instance churn survivable for the rest of the system
- Next chapter: Fault Tolerance & Resilience Patterns — circuit breakers, retries with backoff, and graceful degradation
Fault Tolerance & Resilience Patterns
Distributed Systems & Scalability
Chapter 9 · Fault Tolerance & Resilience Patterns
Software Architecture Fundamentals Chapter 4 measured a downstream slowdown cascading straight up through a chain of synchronous calls. Technical Support's own perfdiag1/incident1 courses spend whole chapters diagnosing exactly this kind of failure after the fact. This chapter builds the four patterns that stop it from reaching that point at all — and verifies what each one actually buys.
Circuit Breakers: Fail Fast Instead of Paying the Full Cost Every Time
0.1s timeout: 10 calls with no circuit breaker took 1,003.5ms total — every single call paid the full timeout cost. The identical 10 calls through a breaker (threshold 3) took 301.1ms — only the first 3 calls paid the full 0.1s cost; the remaining 7 failed instantly once the circuit opened. 3.3× less total time wasted, for the identical, genuinely-failed outcome.
Retries With Backoff: Giving a Struggling Service Room to Recover
0.05 × 2ⁿ seconds between tries) spread across 751.16ms — roughly 117,000× more real time between the first and last attempt, for the same 5 total tries.
Bulkheads: Isolating Resources So One Failure Doesn't Starve Everything
5): a slow, non-critical recommendations service acquires all 5 connections and holds them. A critical checkout request, using the same pool, then tries to acquire a connection and correctly gets False — blocked, purely because an unrelated, lower-priority feature exhausted a resource checkout also needed.
3 and 2 connections respectively): recommendations still exhausts its own pool completely (a 4th request correctly returns False) — but checkout's own separate pool is untouched, and its request correctly returns True. The critical path stayed available specifically because it was never sharing a resource with the failing one.
Graceful Degradation: A Usable Response, Not a Failed One
ReviewService genuinely failing: a resilient version, catching that specific failure and substituting a fallback ({'reviews': [], 'note': 'reviews temporarily unavailable'}), correctly returns the full order and stock data plus the fallback — a real, usable response. A brittle version, with no such handling, correctly fails the entire request — order and stock data included, even though both of those were computed successfully before the review call ever failed.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A circuit breaker saving 3.3× total time on a genuinely failing call chain | Software Architecture Fundamentals Chapter 4's own cascading-call finding — this is the direct, concrete fix for the exact failure that chapter measured |
| A verified retry-storm vs. spread-out backoff, in real measured milliseconds | Chapter 7's own rate-limiting chapter — both are ways of controlling request density against a downstream system, one from the caller's side, one from the receiver's |
| A brittle gateway discarding two successful results because a third call failed | Technical Support's own `incident1`/`perfdiag1` — this exact pattern (one failed dependency taking down an otherwise-healthy response) is precisely the class of ticket those courses' own diagnostic chapters are built to trace back to its cause |
Hands-On Exercises
Using this chapter's own CircuitBreaker, lower failure_threshold to 1 and re-run the 10-call scenario. Verify the total time drops further than this chapter's own 301.1ms result, and explain the tradeoff a threshold of 1 introduces that a threshold of 3 avoids.
Using this chapter's own bulkhead scenario, give recommendations a pool of 4 instead of 3 (checkout stays at 2, for a combined total of 6 — one more than the original shared pool's own capacity of 5). Verify checkout still succeeds regardless of how large recommendations' own pool is.
Using this chapter's own four verified patterns, explain which ONE of them would have been the most direct fix for Software Architecture Fundamentals Chapter 4's own original cascading-call scenario (service A waiting 300ms because service C was slow), and why the other three, while genuinely useful in general, wouldn't have addressed that specific measured problem.
📄 View solutionChapter 9 Quick Reference
- Circuit breaker: stops calling a service that's genuinely failing — verified:
3.3×less total time wasted,7of10calls failing instantly instead of paying a full timeout - Retries with backoff: spaces out retries instead of bursting them — verified:
751msspread vs. a naive retry's0.01msburst for the same 5 attempts - Bulkheads: isolate resource pools per dependency — verified: a shared pool let one feature block another; separate pools kept the critical path available
- Graceful degradation: return a usable partial response instead of failing everything — verified: a resilient gateway kept two working results when a third dependency failed; a brittle one discarded all three
- Next chapter: Capstone — designing a real system at scale, applying every chapter in this course together
Capstone — Designing a System at Scale
Distributed Systems & Scalability
Chapter 10 · Capstone: Designing a System at Scale
One continuous worked project: a URL shortener, assembling every verified pattern from Chapters 1–9 into a single running system — sharded storage, a cache-aside redirect path, an async click-tracking queue, rate-limited creation, and a circuit breaker protecting the critical path from a broken analytics pipeline. Every number below comes from actually running the assembled system, not from restating each chapter's own earlier findings.
Assembling the System
Running the System End to End
create_short_url('https://example.com/a-very-long-article-url') returns short code 'c69915', and store.get('c69915') correctly returns the original long URL — the sharded lookup (Chapter 4's own hash-based routing) found the exact same shard it was written to.
redirect_service.store_lookups at exactly 1 — every redirect after the first was served entirely from cache, matching Chapter 3's own verified cache-aside shape at a different scale.
click_queue.events correctly holds 21 pending events — none of them processed yet, because redirect() only ever calls publish(), never a handler directly. Processing the queue afterward correctly produces click_counts == {'c69915': 21} — every single click accounted for, on its own schedule, exactly matching Chapter 6's own decoupling finding.
create_short_url() against a token bucket with capacity 5: the first 5 succeed, the remaining 3 correctly raise RuntimeError('Rate limit exceeded') — ['created', 'created', 'created', 'created', 'created', 'rate-limited', 'rate-limited', 'rate-limited'], matching Chapter 7's own token-bucket burst-then-throttle shape exactly.
2) and feeding it a handler that always raises ConnectionError: processing 10 queued clicks produces ['failed-full-cost', 'failed-full-cost', 'failed-fast', 'failed-fast', ...] — the circuit opens after 2 real failures, exactly matching Chapter 9's own verified breaker behavior. Meanwhile, calling redirect() 10 more times during this same broken-analytics period: every single one succeeds correctly, and redirect_service.store_lookups stays at 1 — completely untouched. The core redirect functionality never even knows the analytics pipeline exists.
The Rest of the System, Reasoned About Rather Than Re-Verified
Three more chapters shape this same system's real deployment, without needing fresh code to demonstrate here — each one's own mechanism was already verified in its own chapter, applied directly:
| Chapter | How it applies to this exact system |
|---|---|
| Chapter 2 — Load Balancing | Multiple UrlShortener instances behind Least Connections, exactly as verified — redirect traffic vastly outweighs creation traffic in a real URL shortener, so read-heavy load balancing matters most here |
| Chapter 5 — CAP Theorem | Short-code creation should be CP: two clients racing to create a code for the same long URL must never produce two different codes for it, the identical risk Chapter 5 verified for AP-style writes. Redirects, by contrast, can safely be AP — a slightly stale cache entry (Chapter 3) is a tolerable cost for availability, not a hash collision risk |
| Chapter 8 — API Gateway & Service Discovery | A gateway would front create_short_url() and redirect() as separate routes, using discovery to find whichever shard/replica currently holds a given short code — exactly the registry pattern Chapter 8 verified surviving a redeploy |
What This Course Doesn't Cover
This course stayed at the level of verifiable, single-process simulations of each pattern's own core mechanism — it did not cover actually deploying multiple real processes or machines, configuring a real load balancer or message broker, or the operational work of running any of this in production. Software Architecture Fundamentals Chapter 10's own scope note named this course as the next step after getting a system's own shape right; this course, in turn, hands off to actually operating one — Technical Support's own perfdiag1/incident1/netdiag1 courses cover diagnosing a system like this one once it's live and something goes wrong.
Hands-On Exercises
Create two more short URLs through this chapter's own UrlShortener, then redirect all three short codes a mixed number of times (e.g., 5, 3, and 10 redirects respectively). Verify store_lookups is exactly 3 (one genuine miss per distinct code), not 1 and not 18.
Using this chapter's own rate-limited create_short_url(), wait long enough for the token bucket to refill 2 tokens (at 1/sec, this chapter's own configured rate), then attempt 2 more creations. Verify both succeed, confirming the limiter recovers correctly rather than staying permanently exhausted.
Using this chapter's own verified resilience finding, explain specifically WHY redirect()'s own code never needed a try/except around anything analytics-related, when Chapter 9's own graceful-degradation example needed an explicit try/except around a failing dependency. What's structurally different about how this capstone wired click tracking?
Chapter 10 Quick Reference — and Course Quick Reference
- This capstone: a URL shortener assembling Chapters 3, 4, 6, 7, and 9 into one running system — verified: 1 real store lookup across 21 redirects, 21 click events fully decoupled and correctly counted, 5 of 8 rapid creations allowed, and redirects proven immune to a fully broken, circuit-breaker-wrapped analytics pipeline
- Course arc: Ch.1 (why systems need to scale, measured) → Ch.2 (load balancing) → Ch.3 (caching) → Ch.4 (database scaling) → Ch.5 (CAP theorem) → Ch.6 (message queues) → Ch.7 (rate limiting) → Ch.8 (API gateways & discovery) → Ch.9 (resilience patterns) → Ch.10 (assembling it all)
- Where this leads: Technical Support's own diagnostic courses (
perfdiag1,incident1,netdiag1) — this course builds the system; those courses diagnose it once it's live