Module 1: How to Approach a System Design Problem
7. Thinking in tradeoffs and where the box breaks
Description
This lesson installs the mindset that separates someone who recites designs from someone who reasons about them: in system design there's no correct answer, there are tradeoffs. Every decision —301 or 302, SQL or NoSQL, cache or no cache, strong or eventual consistency— gives you something in exchange for giving up something else. There's no option that wins at everything; there's the one that wins at what this system cares about and pays the price this system can pay. That's why the most professional answer to "which database do I use?" isn't "PostgreSQL" or "MongoDB", but "it depends —on what?—". "It depends" isn't evasion: it's the honest acknowledgment that the correct answer depends on the requirements, and that your job is to make the trade explicit, not to pretend there's a magic cost-free option.
And this lesson is also the bridge of the whole module toward the rest of the guide. You'll take the single box you drew in lesson 6 and point out, one by one, the cracks where it breaks —the reads that saturate it, the disk that fills, the single point of failure—, and map each crack to the module that fixes it and to the tradeoff that fix introduces. Because here's the key almost nobody says: each solution brings its own new problem. The cache speeds up the reads in exchange for being able to serve stale data. Replicas spread the load in exchange for propagation delay. Sharding scales storage in exchange for more complicated queries. Scaling Enlace isn't removing problems; it's swapping some problems for others you'd rather have. Seeing that clearly is the mindset leap of this lesson.
Connection to the module: this is the lesson that closes the conceptual arc of module 1. Lessons 1 to 5 gave you the method (understand, ask, scope, the framework); lesson 6 produced the first design (the single box); this one teaches you to reason about it —to see its decisions as trades and its limits as the map of what's coming—. It's the direct antechamber of the mini-project (lesson 8), which will ask you not just to draw a box but to name its tradeoffs and its three breaking points. And it's the hinge toward modules 2 to 7: each one is a "fix a crack in the single box", with its explicit tradeoff. If you understand this lesson, you understand why the guide has the shape it has.
The one-lane bridge
Think of it this way. A town has a river, and to cross it, a bridge. Just one, single-lane. While few cars pass, it's perfect: cheap to maintain, simple, does its job. But the town grows, and the bridge starts showing its limits, all at once. When a lot of traffic arrives, a huge line forms —the bridge saturates, no more cars fit per minute—. When too heavy a truck passes, there's a risk it gives way —there's a capacity limit—. And the most feared: if the bridge is damaged, or has to be closed for repair, the town is cut off —there's no other way to cross the river, it's a single point of failure—.
Now, how does the town fix each problem? And notice that each fix brings its own new problem. For the saturation, they widen the bridge to two lanes —more expensive and more work—. For the weight, they reinforce the structure —more maintenance cost—. For the single point of failure, they build a second bridge downstream —redundancy—, but now they have to decide how to split the traffic between the two (which one do I send each car through?) and maintain both. There's no free fix: each one solves a crack and opens a new decision. The town's engineer doesn't look for "the perfect bridge" —it doesn't exist—; they look, for each real problem, for the fix whose price the town is willing to pay.
Enlace in a single box is that one-lane bridge. It works today, and its cracks are the same: it saturates with reads, its disk fills, and it's a single point of failure. Each fix —cache, replicas, sharding, redundancy— solves a crack in exchange for a new problem (stale data, delay, complex queries, coordination). Thinking in tradeoffs is exactly this: seeing that there's no perfect bridge, that each improvement has its price, and that designing is choosing what price you pay for what improvement, according to what your town —your system— needs.
It's worth spelling it out in full:
There's no correct design, there are tradeoffs: each decision gives something in exchange for giving up something else, and each solution brings its own new problem. "It depends" is the professional answer; your job is to make the trade explicit, not to find the cost-free option (it doesn't exist).
How a tradeoff is named
A badly stated tradeoff sounds like an opinion ("MongoDB is better"). A well-stated tradeoff has a precise form: "I choose A over B; I gain X, I pay Y; I choose it because for this system X matters more than Y". The four parts —the option, what you gain, what you pay, and why the balance is worth it here— are what turn a preference into an engineering decision. Let's see it with Enlace's first tradeoff, the one that already appeared in lesson 1: 301 vs. 302 for the redirect.
- 301 (permanent): the browser caches the redirect. You gain: speed and less load —repeat visits don't even reach Enlace, the browser goes straight—. You pay: you lose the count of those visits —if the browser doesn't go through Enlace, you can't count the click— and you can't change where the link points (the browser has the old one cached).
- 302 (temporary): the browser does not cache. You gain: every visit goes through Enlace, so you can count every click and change the destination whenever you want. You pay: more read load on Enlace —all visits, even repeat ones, hit the server—.
Let's put numbers on the "you pay", because a tradeoff with numbers is a decision and without numbers is a chat:
# the 301 vs 302 tradeoff, quantified
qps_read = 3858 # ~4000 reads/s of Enlace
# suppose a fraction of the visits are "repeat"
# (the same browser returns to the same link and a 301 would save Enlace from them)
for repeat_frac in [0.30, 0.50, 0.70]:
load_302 = qps_read # 302: all reach Enlace
load_301 = qps_read * (1 - repeat_frac) # 301: the repeats are cached by the browser
print(f"repeats {repeat_frac:.0%}: "
f"302 -> Enlace sees {load_302:.0f}/s (counts all clicks) | "
f"301 -> Enlace sees {load_301:.0f}/s (saves {repeat_frac:.0%}, loses that count)")
What to expect. Running this with Python 3.14.0:
repeats 30%: 302 -> Enlace sees 3858/s (counts all clicks) | 301 -> Enlace sees 2701/s (saves 30%, loses that count)
repeats 50%: 302 -> Enlace sees 3858/s (counts all clicks) | 301 -> Enlace sees 1929/s (saves 50%, loses that count)
repeats 70%: 302 -> Enlace sees 3858/s (counts all clicks) | 301 -> Enlace sees 1157/s (saves 70%, loses that count)
There's the tradeoff, measured: if 50% of the visits are repeat, a 301 saves Enlace half the read load (from ~3858 to ~1929/s) —an enormous scale gain— but in exchange for losing the count of that half of the visits. Which do you choose? It depends —and now "it depends" has content—: if Enlace cares more about scale and speed than precise analytics, choose 301; if it cares about counting every click (because analytics is the product), choose 302 and pay the extra load. Notice that neither option is "correct" in the abstract: the correct one depends on what this system values. Since Enlace v1 deferred analytics (lesson 4), here a 301 is defensible —we don't need to count clicks yet, so we take advantage of the load savings—. That's the complete form of a tradeoff: option, you gain, you pay, and why the balance is worth it here.
Where the single box breaks (the guide's map)
Now the bridge toward the rest of the guide. Let's take the single box from lesson 6 and go through its four cracks, each with the module that fixes it and the tradeoff that fix introduces. This table is, literally, the index of modules 4 to 7 seen as answers to the limits of the single box:
| Crack in the single box | The number that reveals it | Module that fixes it | Tradeoff of the fix |
|---|---|---|---|
| Reads saturate it | ~4000/s, no margin for spikes | M4: cache | Speed in exchange for being able to serve stale data (invalidation) |
| The disk fills | 1.23 TB/year; 6 TB at 5 years | M5: replicas + sharding | Storage scale in exchange for replica delay (lag) and more complex queries |
| One server isn't enough for compute | spikes > 4000/s of reads | M6: balancing + stateless | Horizontal scale in exchange for having to take the state out of the server (no local sessions) |
| Single point of failure | 99.9% requires < 8.76 h down/year | M7: redundancy + failover | Availability in exchange for coordination complexity and the consistency tradeoff (CAP) |
Read it as a story. Enlace starts in one box (M1). Module 2 puts numbers on everything. Module 3 solves how to generate the short_codes and model the data. And then modules 4 to 7 fix, in order, the four cracks: first the most urgent (the reads → cache), then the storage (→ replicas/sharding), then the compute (→ balancing), and finally the reliability (→ redundancy and consistency). Each arrow in the following diagram is a module, and each module pays a price for its improvement:
graph TD
M1["M1: one box<br/>(server + DB)"] --> M3["M3: data model<br/>+ generate short_code"]
M3 --> M4["M4: + cache<br/>(gains speed,<br/>pays stale data)"]
M4 --> M5["M5: + replicas/sharding<br/>(gains storage,<br/>pays lag and complexity)"]
M5 --> M6["M6: + balancing/stateless<br/>(gains horizontal scale,<br/>pays taking out the state)"]
M6 --> M7["M7: + redundancy<br/>(gains availability,<br/>pays consistency: CAP)"]
Notice the beauty of the matter: the single box, drawn honestly in lesson 6, already contained the blueprint of the whole guide. We didn't invent modules 4 to 7 at random; they came from looking at the cracks of the simple box and asking ourselves "how do you fix this, and what does it cost?". That's the method in its purest form: start simple, find where it breaks, fix the most urgent crack, and repeat —always knowing what price you pay for each improvement—.
"It depends" is the professional answer
It's worth pausing on why "it depends" isn't an evasion but the mature answer. When someone asks "which is better, SQL or NoSQL for Enlace?", the novice answer is to pick one and defend it as if it were universally superior. The professional answer is: "it depends on what you prioritize", and then make the dependencies explicit:
- If you prioritize simple lookups by key (which is what
resolvedoes: look up byshort_code), almost either of the two works, and the choice is decided by other factors. - If you prioritize strong consistency and transactions (a just-created link has to exist immediately), traditional SQL gives it to you more easily.
- If you prioritize scaling the write horizontally without pain (imagine the writes were millions/s, which isn't Enlace's case), many NoSQL systems are designed for that.
Since Enlace has ~40 writes/s (low), lookups by simple key, and tolerates eventual consistency, the decision can be made on operational simplicity rather than scale —and that's exactly the kind of reasoning of module 3—. What matters here isn't the answer, it's the form: "it depends on X, Y, Z; for Enlace, X and Y point to this". Whoever answers that way shows they understand there are no magic options, only contextual trades. Whoever answers "MongoDB, always" shows they memorized an answer without understanding the question.
There's a trap to avoid: "it depends" has to be followed by "on what?". "It depends" on its own, without naming what it depends on or resolving it for the concrete case, really is evasion. The professional version always lands: it names the dependencies, looks at the system's requirements, and decides —justifying the trade—. Depending isn't not deciding; it's deciding with the reasons in plain sight.
Common mistakes
Looking for the "best" option in the abstract. What happens: someone asks or answers "what's the best database / the best language / the best architecture?" as if there were an answer independent of the problem. They end up defending a technology out of fashion or habit, not fit to the requirements. Why it happens: it's comfortable to have a fixed answer to apply every time; thinking in tradeoffs each time is tiring. How to detect it: if your answer to "what do I use?" doesn't change according to the system's requirements, you're reciting, not designing. How to fix it: train the reflex of "it depends —on what?—", name the dependencies, and decide for the concrete case. The best option is always for these requirements, never in a vacuum.
Believing that scaling removes problems (when it swaps them). What happens: someone adds a cache and thinks "that's it, faster and free", without realizing they now have the invalidation problem (what happens when the data changes and the cache has the old one?). Or they add replicas without accounting for the lag. Each improvement solved a crack and opened another they didn't see coming. Why it happens: solutions are presented by their benefit, and their cost is less visible. How to detect it: if when adding a component you can't name the new problem it introduces, you didn't understand the tradeoff. How to fix it: for each improvement, ask yourself "what new problem do I bring?". Cache → stale data. Replicas → lag. Sharding → complex queries. Redundancy → coordination and consistency. Scaling is swapping problems for others you'd rather have, not eliminating them.
Saying "it depends" and stopping there. What happens: someone learns that "it depends" is the mature answer and uses it as a shield to avoid committing: "SQL or NoSQL?" → "it depends" —and that's it—. That's not design, it's dodging the decision. Why it happens: acknowledging that there are tradeoffs is confused with not resolving them. How to detect it: if your "it depends" isn't followed by the concrete dependencies and a decision for the case, it's evasion. How to fix it: "it depends" is the start of the answer, not the end. Always follow with "on what? —on X and Y— and for this system, X and Y point to this option, paying this price". Name the trade and choose.
Exercises
Exercise 1 — Name a complete tradeoff. Take the decision "put a cache in front of Enlace's database" and write it as a complete tradeoff with its four parts: (1) the option, (2) what you gain, (3) what you pay (the new problem), (4) why the balance is worth it for Enlace. (Hint: Enlace is read-heavy, 100:1.)
See solution
A well-named tradeoff:
(1) Option: put an in-memory cache in front of the database, serving the most-queried links without touching the DB. (2) You gain: speed and scale on the read path. Since Enlace is 100:1 (read-heavy) and a few links concentrate most of the visits, serving those from memory offloads almost all the DB's load —the ~4000 reads/s stop hitting the disk—. (3) You pay: the invalidation problem. The cache can have an old copy of data that changed in the DB (for example, if a link is deleted or its destination changes, the cache might keep serving the old one until it expires). Now you have two copies of the data that can disagree. (4) Why it's worth it for Enlace: because in Enlace the data almost never changes —once created, a link always points to the same destination—, so the risk of serving stale data is low, while the scale gain is enormous (100:1). The trade is very clearly in favor of the cache. If Enlace were a system where the data changes constantly, the price of invalidation would be much higher and it would need more thought.
Notice that part (4) is what turns the generic tradeoff ("cache = speed vs. stale data") into a decision for this system ("and since Enlace hardly changes its data, the balance is worth it"). Without that part, it's theory; with it, it's design.
Exercise 2 — Map the crack to the module. For each symptom of Enlace's single box, say which crack it is, which module it sends you to, and —the important part— what new problem the fix introduces: (a) "At peak hour, the reads exceed what the DB can handle and the redirects become slow." (b) "The single machine's disk is about to fill up." (c) "If the single machine reboots, Enlace is down and we don't meet the 99.9%."
See solution
- (a) Reads that saturate → M4 (cache). New problem of the fix: invalidation / stale data. Serving from memory, you run the risk of delivering an outdated copy if the data changed. (In Enlace the risk is low because links hardly change, as we saw.)
- (b) Disk that fills → M5 (replicas and sharding). New problem: replication lag (a replica can be a bit behind the primary) and more complex queries (with sharding, the data is spread out and some operations have to query several nodes). You scaled the storage, but now you coordinate several machines.
- (c) Single point of failure → M7 (redundancy and failover). New problem: the consistency tradeoff (CAP). By having several copies of the system so you don't go down, when the network between them fails you have to choose between continuing to respond with possibly inconsistent data (availability) or rejecting requests so as not to give incorrect data (consistency). You can't have both at 100% during a partition. That's the heart of module 7.
The lesson: each fix is a tradeoff, not a clean victory. Recognizing the new problem of each solution —invalidation, lag, CAP— is what prepares you for the modules to come, where each is studied in depth.
Exercise 3 — "It depends" with content. A colleague asks you: "For Enlace, do we use strong consistency (a just-created link is seen instantly across the whole system) or eventual consistency (it may take ~1 second to propagate to all replicas)?". Answer in the professional style: start with "it depends", name what it depends on, look at Enlace's requirements, and decide, justifying the trade.
See solution
A professional answer:
"It depends on how serious it is that a just-created link takes a moment to work everywhere. With strong consistency, the moment
shortenreturns the code, we guarantee thatresolvefinds it on any replica instantly —but that costs coordination between replicas on every write, which adds latency and reduces availability if the network between them fails—. With eventual consistency, a new link may take ~1 second to appear on all replicas —in exchange for faster writes and greater availability—.For Enlace, look at the requirement: what really happens if a link takes a second to propagate? The user who just created it maybe shares it and whoever opens it in that first second, on a distant replica, gets a momentary 404 —annoying, but not catastrophic, and it resolves itself in a second—. Enlace isn't a banking system where stale data costs money; it's a redirector where a second of delay on a new link is tolerable. So I choose eventual consistency: I pay that small risk (a fleeting 404 in the first second of a link's life) in exchange for fast writes and high availability, which is what a read-heavy service like Enlace values most. If this were a system where outdated data caused real harm, I'd choose strong and pay the latency."
Notice the complete form: it starts with "it depends", names the dependency (how serious the delay is), lands on Enlace's requirement (a fleeting 404 is tolerable), and decides (eventual) justifying the trade (speed and availability in exchange for a small, acceptable risk). That's "it depends" with content —module 7 develops this reasoning in depth with CAP and PACELC—.
Summary and next step
In this lesson you installed the central mindset of the craft: there's no correct design, there are tradeoffs, like the one-lane bridge where each fix has its price. You learned to name a tradeoff with its four parts —option, what you gain, what you pay, why it's worth it here— and you measured it with the 301 vs. 302 case: a 301 saves Enlace up to half the read load, in exchange for losing the count of those visits —defensible in v1, which deferred analytics—. And you saw the truth almost nobody says: each solution brings its own new problem —cache→stale data, replicas→lag, sharding→complexity, redundancy→consistency (CAP)—; scaling is swapping problems for others you'd rather have, not eliminating them.
And you crossed the bridge toward the rest of the guide: you took the single box from lesson 6 and mapped its four cracks to the modules that fix them (M4 cache, M5 replicas/sharding, M6 balancing, M7 redundancy), each with its tradeoff. The single box, drawn honestly, already contained the blueprint of the whole guide. And you sharpened the professional answer —"it depends —on what?—", which names the dependencies and decides, instead of dodging—.
Before moving on you should be able to: explain why there's no "correct" design; name a tradeoff with its four parts; map each crack of the single box to its module and to its new problem; and answer "it depends" with content (dependencies + justified decision).
What comes next is putting the whole module together with your own hands. Lesson 8 is the mini-project: you take a vague prompt and produce the complete starter package —functional and non-functional requirements with numbers, clarifying questions with assumptions, scope table, computed estimation, single-box diagram, and the three points where it will break—. It's the whole of module 1, applied from start to finish, and your preparation for the in-depth estimation of module 2.
Resources
- Designing Data-Intensive Applications (DDIA), Chapter 1 — official site — Kleppmann's entire thesis is that designing data systems is an exercise in tradeoffs between reliability, scalability, and maintainability. No chapter gives you "the correct answer"; they all give you the trades. It's the definitive source of this lesson's mindset.
- System Design Primer — "Step 4: Scale the design" — where the Primer enumerates how to scale (cache, replicas, sharding, balancing) and warns, for each technique, of its downsides. It's the list version of the "each solution brings its new problem" that we mapped with the single box.
- Martin Kleppmann — "A Critique of the CAP Theorem" — the consistency tradeoff (CAP) that appears in the last crack of the single box, explained rigorously by the author of DDIA. You don't need to read it whole now; save it for module 7, where this tradeoff is the protagonist.
- MDN —
301vs302— go back to the definition of the two redirects now that you understand the quantified tradeoff (load savings vs. click counting). Seeing the specification with the trade in mind gives new meaning to the cacheability difference.