Module 8: Project — Design Enlace End to End
3. Step 2 — The capacity table, executed
Description
With the step 1 contract in hand, it is time for step 2 of the framework: turning the requirements into capacity numbers. It is the napkin math that module 2 developed in depth, and here you run it end to end for Enlace in a single program, producing the second artifact of the deliverable: the capacity table. The hard discipline of the guide rules with all its force in this lesson: every number is executed, not quoted. You are not going to write "~4,000 reads/s" because you remember it; you are going to run 100_000_000 / (30·24·3600) · 100 and read 3858 in the output. The table you produce is the design's checksum: if your calculations reproduce the anchor numbers, you are on track; if not, there is an error and the table gives it away.
In this lesson you run the four capacity calculations —QPS (read and write), storage at 5 years, bandwidth, and memory of the working set— in a single script, and you obtain the real table. But the most important thing is not the digits: it is learning to read each number as a signal of where to design. The ~40 writes/s say "the write path is calm"; the ~4,000 reads/s say "deep-dive here, in cache and load balancing"; the 6 TB say "replicas and sharding"; the ~333 MB say "the cache is dirt cheap". Step 2 does not produce numbers for the sake of it; it produces the blueprint of decisions that step 3 is going to draw.
Connection to the module: this is the second lesson of the journey, and the bridge between the contract (lesson 2) and the diagram (lesson 4). It takes the non-functional requirements of step 1 as inputs of the calculations, and their results will be the justifications of each box you draw afterward. It is also the lesson that most honors the rule of the guide —reproduce the arithmetic, do not trust memory—, and the one that leaves you a reusable Python template to estimate any system, not just Enlace.
The budget before the move
Think of it this way. You are going to move house and you hire a moving company. Before booking the truck, there is a calculation that decides everything else: how much fits and how much does what you have to move weigh? You do not do it by eye —"well... a lot"—, because if you underestimate, the truck comes up small and you have to make two trips; if you overestimate, you pay for a semi-trailer to move four boxes. You get out the tape measure and the calculator: so many cubic meters of furniture, so many kilos of books, so many fragile boxes. Those numbers dictate the decision: a small truck if the result is "fits in 10 m³", a big one if it is "50 m³", two trucks if it is "100 m³". The calculation goes before booking, and the booking is derived from the calculation.
Notice two things about that calculation. First: you look for the order of magnitude, not the exact digit. "About 40 m³" is the useful answer; "43.7 m³" is false precision —the tape measure of a house does not give three significant figures, and it does not matter, because the truck comes in sizes of 20, 40 and 60 m³—. Second: each number signals a decision. The cubic meters decide the truck size; the kilos decide whether you need more movers; the fragile boxes decide how much packing material to buy. You do not calculate for the sake of calculating; you calculate to decide.
Capacity estimation is that pre-move calculation. Before drawing the architecture (booking the truck), you compute how much Enlace "weighs and fits": how many operations per second (the traffic volume), how many terabytes (the data volume), how many MB/s over the network (the weight that travels), how many MB of working set (what fits in the cache). And each number dictates a design decision, just as the cubic meters dictate the truck. You look for the order of magnitude —~4,000 reads/s, not 3,858.02—, because the architecture comes in discrete "sizes" (one box, with cache, with replicas, sharded) and the number tells you which. The capacity table is the budget of Enlace's move.
It is worth spelling it out:
Capacity estimation goes before the design and dictates it. It is computed, not quoted: each number is reproduced with arithmetic. And each number is a signal —not an isolated datum— that points to a design decision: the read QPS asks for cache and load balancing, the storage asks for sharding, the working set memory says how much the cache costs. The capacity table is the blueprint from which the architecture comes.
The four calculations, in a single program
Let us run all of step 2 for Enlace. A single script, four blocks —QPS, storage, bandwidth, memory—, with the inputs from step 1 at the very top. I actually ran it with Python 3.14.0 before writing this lesson; the output below is literal.
# capacity_table.py — Enlace's step 2, end to end.
# Inputs (from step 1): 100M URLs/month, 100:1 ratio, ~1 KB/record, 5 years.
writes_per_month = 100_000_000
read_write_ratio = 100
bytes_per_record = 1024 # ~1 KB per Link record
payload_bytes = 500 # the long_url, the payload that dominates
retention_years = 5
# --- 1. QPS (operations per second) ---
seconds_per_month = 30 * 24 * 3600 # ~2.6 million
qps_write = writes_per_month / seconds_per_month
qps_read = qps_write * read_write_ratio
# --- 2. Storage at N years ---
records_total = writes_per_month * 12 * retention_years
storage_bytes = records_total * bytes_per_record
code_space = 62 ** 7
fraction_used = records_total / code_space
# --- 3. Bandwidth (QPS x payload) ---
read_bw = qps_read * payload_bytes # egress: redirects
write_bw = qps_write * payload_bytes # ingress: new URLs
read_bw_peak = read_bw * 3 # 3x peak
# --- 4. Working set memory (80/20 rule) ---
new_links_per_day = writes_per_month / 30
working_set = new_links_per_day * 0.20 # the hottest 20%
cache_entry_bytes = 500
cache_mem = working_set * cache_entry_bytes
print("=== ENLACE CAPACITY TABLE ===\n")
print(f"writes/s = {qps_write:8.1f} -> ~40/s (calm path)")
print(f"reads/s = {qps_read:8.0f} -> ~4000/s (BOTTLENECK: cache+lb)")
print(f"records at 5 years= {records_total:>14,}")
print(f"storage = {storage_bytes/1e12:8.2f} TB -> ~6 TB (replicas/sharding)")
print(f"code space = {code_space:>14,} (62^7)")
print(f"fraction used = {fraction_used:8.4%} -> plenty to spare")
print(f"read bandwidth = {read_bw/1e6:8.2f} MB/s (peak {read_bw_peak/1e6:.0f} MB/s)")
print(f"write bandwidth = {write_bw/1e3:8.0f} KB/s (the network is NOT a bottleneck)")
print(f"working set = {working_set:>14,.0f} entries")
print(f"cache memory = {cache_mem/1e6:8.0f} MB -> ~333 MB (fits in RAM)")
What to expect. Running python capacity_table.py with Python 3.14.0, exactly this comes out:
=== ENLACE CAPACITY TABLE ===
writes/s = 38.6 -> ~40/s (calm path)
reads/s = 3858 -> ~4000/s (BOTTLENECK: cache+lb)
records at 5 years= 6,000,000,000
storage = 6.14 TB -> ~6 TB (replicas/sharding)
code space = 3,521,614,606,208 (62^7)
fraction used = 0.1704% -> plenty to spare
read bandwidth = 1.93 MB/s (peak 6 MB/s)
write bandwidth = 19 KB/s (the network is NOT a bottleneck)
working set = 666,667 entries
cache memory = 333 MB -> ~333 MB (fits in RAM)
There is Enlace's entire capacity table, executed. Each number reproduces the guide's checksum, and —what matters— each one speaks to the design that is coming. Let us read it row by row, not as digits but as signals.
Reading the table: each number is a signal
~40 writes/s → the write path is the calm one. 100 million a month, spread over the ~2.6 million seconds of the month, give 38.6 writes/s. It is very little —a single database (one primary) absorbs it without breaking a sweat—. Design signal: no need to deep-dive into the write. The single primary suffices; the effort goes elsewhere.
~4,000 reads/s → here is the bottleneck, here you deep-dive. Since people read 100 times more than they write, the reads are 40 × 100 = 3858/s. A hundred times the writes. This is the row that governs the design: it says, with a number, that Enlace is read-heavy and that almost all the effort goes to the read path. Signal: cache (M4) and load balancing (M6) come from here. When in lesson 4 you draw a cache, this is the number that justifies it.
6,000 M records ≈ 6 TB → replicas and sharding. Five years accumulating 100M/month are 6,000 million records; at ~1 KB each, 6.14 TB. Too much for a machine to be comfortable forever. Signal: the storage has to be distributed (M5) —a single DB does not hold it at 5 years—.
62⁷ ≈ 3.52 × 10¹², 0.17% used → the IDs are plentiful. The 7-character base62 code space is 3.5 trillion; the 6,000 M records use less than 0.2%. Signal: ID generation has plenty of margin —7 characters are more than enough, any strategy (counter, random) fits without fear of running out—.
~2 MB/s of reads (6 MB/s peak) → the network is NOT the bottleneck. The bandwidth is QPS × payload: 4,000/s × 500 B = 2 MB/s, and at peak 6 MB/s —less than 5% of a 1 Gbps NIC (125 MB/s)—. A negative signal, and those count too: there is no need to optimize transfer, nor a CDN for throughput, nor compression for the network. The bandwidth discards a whole family of concerns with one calculation. In design, knowing what to ignore is as valuable as knowing what to attack.
~333 MB of working set → the cache is dirt cheap. With 3,333,333 new links a day and the 80/20 rule (the hottest 20% concentrates the reads), the hot working set is ~666,667 entries, and at ~500 bytes each, ~333 MB —about 0.005% of the total 6 TB—. Signal: the cache fits in a modest Redis (1–2 GB, cheap); there is no need to cache the 6 TB, only the hot little piece. This row is what makes the cache of the row "~4,000 reads/s" not just desirable but cheap.
Notice the pattern: the table is not a list of data, it is a map of where to design. Two positive signals (cache, replicas/sharding) and one negative (the network does not matter) come from six calculations. Step 3 (the diagram) is not going to invent anything; it is going to draw what these signals dictate. That is why step 2 goes before step 3: it is the budget that decides the truck.
One more row: peak versus average
The table above uses averages, and for the order of magnitude that is fine. But a good design sizes for the peak, not for the average, because real traffic is not flat —it has high hours, and a link can go viral—. The napkin rule: multiply the average by a peak factor (3× is a typical and conservative value) and verify that the design still holds.
# peak.py — peak versus average, to size with margin
qps_read_avg = 3858
peak_factor = 3
qps_read_peak = qps_read_avg * peak_factor
reads_to_db_avg = qps_read_avg * (1 - 0.90) # with cache at 90%
reads_to_db_peak = qps_read_peak * (1 - 0.90)
print(f"reads/s average : {qps_read_avg:,}")
print(f"reads/s peak (3x): {qps_read_peak:,}")
print(f"to the DB average: {reads_to_db_avg:,.0f}/s (with cache 90%)")
print(f"to the DB peak : {reads_to_db_peak:,.0f}/s (with cache 90%)")
What to expect. With python peak.py:
reads/s average : 3,858
reads/s peak (3x): 11,574
to the DB average: 386/s (with cache 90%)
to the DB peak : 1,157/s (with cache 90%)
Read it for what it tells the design. At the peak, Enlace sees ~11,574 reads/s instead of 3,858 —three times more—, but with the cache at 90% hit ratio, the database gets 1,157/s at the peak instead of 386/s on average. Signal: the database design (replicas) must be sized for the peak (1,157/s), not for the average (386/s), with extra margin in case the hit ratio drops. This is the honest number that lesson 7 will use to count replicas —and the one that separates a design that holds the viral day from one that only holds the calm day—.
The table as a deliverable
Those numbers, presented as the artifact that goes into the reference design, look like this —one row per magnitude, with its calculation and its signal—:
| Magnitude | Value | Calculation | Design signal |
|---|---|---|---|
| Writes/s | ~40 | 100M / (30·24·3600) | Calm path; one primary suffices |
| Reads/s | ~4,000 | 40 × 100 (100:1 ratio) | Bottleneck: cache + load balancing |
| Reads/s peak | ~11,600 | 3,858 × 3 | Size replicas for the peak |
| Storage | ~6 TB | 100M × 12 × 5 × 1 KB | Replicas + sharding |
| Code space | 62⁷ ≈ 3.5×10¹² | 62 ** 7 (0.17% used) | IDs plentiful; 7 chars ample |
| Bandwidth (read) | ~2 MB/s (6 peak) | 4,000 × 500 B | Network is NOT a bottleneck |
| Working set / cache | ~333 MB | 666,667 × 500 B | Cache dirt cheap (Redis 1 GB) |
Seven rows, and with them the blueprint of the design is complete. Each box you draw in lesson 4 will be able to point to one of these rows as its justification. That is the power of step 2: it turns a contract (step 1) into a set of actionable signals, and it does so with arithmetic anyone can reproduce —including you, if you doubt a number, you run it again—.
Common mistakes
Quoting the numbers from memory instead of executing them. What happens: someone writes "~4,000 reads/s, ~6 TB, ~333 MB" in their table because they remember them from the previous modules, without running the calculations. It works until the prompt changes a datum —"now it is 200M/month"— and the memorized numbers no longer apply, but they are copied anyway because nobody recalculated them. Why it happens: memory is comfortable and the anchor numbers stick. How to spot it: if you cannot produce the script's output, you did not execute step 2. How to fix it: run the table always, even if you "already know" the result. The value of step 2 is not having the numbers, it is deriving them from the inputs —that way, when the inputs change, the numbers change on their own and the design stays correct—. Reproducing the checksum is the discipline; quoting it is faking it.
Dragging false precision. What happens: someone reports "3858.024 reads/s" and "6.144 TB" as if they were exact data, when the input ("100M a month", "100:1 ratio") was already a rough approximation. The precision of the output cannot exceed that of the input. Why it happens: the calculator spits out many digits and they are confused with exactness. How to spot it: if you drag decimals in a napkin estimate, you have excess precision. How to fix it: round to friendly orders of magnitude (~40, ~4,000, ~6 TB, ~333 MB) for the decisions, even if the script prints the decimals. The design decision is made by the order of magnitude, not the fourth figure: between ~4,000 and ~11,600 reads/s there is a decision (size for the peak); between 3858 and 3859 there is none.
Producing numbers without reading them as signals. What happens: someone runs the table, obtains the six correct numbers, and moves to the diagram without asking themselves what each one says —they draw cache, replicas and load balancing "because the canonical shortener has them", not because they read that ~4,000 reads/s ask for them—. The table remains decoration, not a blueprint. Why it happens: computing feels like the work; interpreting is skipped. How to spot it: if you cannot say, for each component of the diagram, which row of the table justifies it, you did not read the table. How to fix it: for each number, write its signal —"~4,000/s → cache"; "6 TB → sharding"; "2 MB/s → the network does not matter"—. Step 2 does not end when the numbers come out; it ends when each number points to a decision.
Exercises
Exercise 1 — Reproduce two rows by hand. Without running the script, compute with pencil and paper: (a) Enlace's writes/s (100M/month, month ≈ 30 days); (b) the storage at 5 years (100M/month, ~1 KB/record). Show the calculations and round to friendly orders of magnitude. Then say what design signal each result gives.
See solution
- (a) Writes/s. Seconds in a month =
30 × 24 × 3600 = 2,592,000(~2.6 M). Writes/s =100,000,000 / 2,592,000 ≈ 38.6, rounded ~40/s. Signal: the write path is calm; a single primary suffices, no need to deep-dive there. - (b) Storage at 5 years. Records =
100M × 12 months × 5 years = 6,000,000,000(6,000 M). Bytes =6,000,000,000 × 1,024 ≈ 6.14 × 10¹², rounded ~6 TB. Signal: too much for a machine to be comfortable; replicas and sharding (M5).
It matches what Python computed (38.6 and 6.14 TB). Notice the napkin trick: "100M over ~2.6M ≈ 40" gives you the order of magnitude without an exact calculator, and that is enough for the decision —the truck is chosen by "40 m³", not by "43.7"—.
Exercise 2 — Re-estimate for an Enlace 5× larger. The team projects that Enlace will grow to 500 million new URLs/month (5×), keeping the 100:1 ratio and ~1 KB/record. Recompute: (a) writes/s, (b) reads/s, (c) storage at 5 years. Does any design signal change relative to the 100M/month Enlace? Do 7 characters still suffice for the short_code?
See solution
- (a) Writes/s:
500,000,000 / 2,592,000 ≈ 193/s(~200/s). Still little for a DB, but no longer "ridiculous": 5× the previous ones. - (b) Reads/s:
193 × 100 ≈ 19,300/s(~20,000/s). Here the scale of the bottleneck does change: five times more reads. - (c) Storage at 5 years:
500M × 12 × 5 = 30,000 M records × 1 KB ≈ 30 TB.
Signals that change: the reads rise to ~20,000/s (the cache and replica design needs more nodes, but the shape of the design does not change —it is still cache + replicas + sharding—), and the storage rises to ~30 TB (more shards). The signal that does not change is the code one: do 7 characters suffice for 30,000 M records? 62⁷ = 3,521,614,606,208; 30,000,000,000 / 3,521,614,606,208 ≈ 0.85% used. Still below 1%, so 7 characters are more than enough even at 5× the scale. The lesson: scaling the anchor numbers changes how many nodes, not what shape the design has —and it confirms how robust choosing 7 characters is—.
Exercise 3 — The negative signal that saves work. A colleague, upon seeing Enlace's 6 TB and 4,000 reads/s, proposes putting a global CDN and aggressive network compression into the design "to hold the bandwidth". Using the bandwidth row of the table, explain why that concern is misdirected, what number refutes it, and what general lesson about estimating this case teaches.
See solution
The concern about bandwidth is misdirected, and the table refutes it with a number: Enlace's read bandwidth is 4,000/s × 500 B = 2 MB/s on average, 6 MB/s at peak —less than 5% of a single 1 Gbps NIC (125 MB/s)—. The network is plentiful for Enlace by almost two orders of magnitude. A CDN for throughput and aggressive compression solve a problem Enlace does not have: its payload is small text (a URL of ~500 B), not multimedia. Throwing in those components would be complexity with no number to justify it —exactly the bloated-design mistake—.
The general lesson: an estimate not only says where to put effort, it also says where not to. Enlace's bandwidth row is a negative signal —"the network is not the bottleneck"— and those signals are just as valuable as the positive ones, because they discard whole families of concerns (CDN for throughput, compression, geo-distribution for bandwidth) with a single calculation. Designing well is as much knowing what to attack (the read, with cache) as knowing what to ignore (the network). If the colleague had read the table instead of just looking at the 6 TB, they would have seen that the bandwidth was already solved by the physics of the problem. Step 2 does not end at the numbers; it ends at the signals, and one of Enlace's signals is "do not spend a minute on the bandwidth".
Summary and next step
In this lesson you executed step 2 of the framework: Enlace's capacity table, run in Python end to end, the second artifact of the deliverable. You reproduced —not quoted— the six anchor numbers: ~40 writes/s (38.6), ~4,000 reads/s (3858), ~6 TB (6.14) at 5 years, 62⁷ = 3,521,614,606,208 with 0.17% used, ~2 MB/s of read bandwidth, and ~333 MB of working set. And you added the peak row (×3): ~11,600 reads/s, ~1,157/s to the database with cache.
The most important thing: you learned to read each number as a signal, not as a digit. The ~40 writes/s say "calm path, one primary suffices"; the ~4,000 reads/s say "the bottleneck, deep-dive into cache and load balancing"; the 6 TB say "replicas and sharding"; the ~333 MB say "the cache is dirt cheap"; and the 2 MB/s say "the network does not matter" —a negative signal that saves work—. With the move budget, you saw why step 2 goes before step 3 and dictates it.
Before moving on you should be able to: run the capacity table and reproduce its six numbers; distinguish average from peak and why you size for the peak; and —the essential thing— name the design signal of each number, including the negative ones.
What comes next is drawing. In lesson 4 you execute step 3: Enlace's high-level design, with its mermaid diagram of the complete distributed architecture. And you are not going to invent anything —you are going to draw exactly what the signals of this table dictate—: a cache because the 4,000 reads/s ask for it, replicas and sharding because the 6 TB require them, a load balancer because the compute also scales. Each box of the diagram will have, behind it, a row of the table you just executed.
Resources
- System Design Primer — "Back-of-the-envelope calculations" and "Powers of two / Latency numbers" — the reference tables (powers of two, latency numbers, figures per second) that make napkin math fast. The practical complement of this lesson for estimating without a calculator.
- Designing Data-Intensive Applications, Kleppmann — Chapter 1, "Describing Load" and "Describing Performance" — how load is formally characterized (the "load parameters", the equivalent of Enlace's QPS and ratio) and performance (throughput vs. percentiles). The theoretical framework behind the rows of the capacity table.
- Jeff Dean — "Numbers Everyone Should Know" (Latency numbers) — the canonical list of latencies (RAM ~100 ns, SSD ~150 µs, disk ~10 ms, network within the datacenter ~500 µs) that grounds the values
L_cache = 1 msandL_db = 50 msthat the table uses and that lesson 6 is going to exploit. Keeping it in mind calibrates every latency estimate.