Module 5: Scaling the Database
8. Project: a sharding and replica plan for Enlace
Description
The moment has come to bring the module's seven lessons together into a single deliverable: the scaling plan for Enlace's database. In this project you're going to do what an engineer does when a single database stops being enough: decide, with numbers, how many shards, how many replicas per shard, where each read and each write goes, how you handle the replication lag, and how you add capacity without unleashing a remapping storm. It's not "let's add more machines and see"; it's a defensible topology where every number comes from back-of-the-envelope arithmetic and from code that runs.
The deliverable has four parts, like a real design doc: (1) the topology diagram (shards + replicas + routing), (2) the capacity table (shards for the 6 TB, replicas for the 100:1, load per node), (3) the run demonstration that growing with consistent hashing moves ~K/N and not almost all, and (4) the list of tradeoffs —what you chose, what it cost, and what would change it—. By the end you'll have a plan you'd know how to present in a design review and that serves as a template for scaling the database of any read-heavy system, not just Enlace's.
Connection to the module: this is M5's capstone. It uses the pressure of lesson 2 (when a DB isn't enough), the replication of lessons 3-4 (primary/replica, lag), the sharding of lesson 5 (key = short_code, by hash), and the problem/remedy pair of lessons 6-7 (mod-N remaps almost everything; consistent hashing only ~K/N). And it closes the guide's scaling arc: the cache (M4) was the first line of defense; replicas and sharding are the second, for the load the cache doesn't absorb and for the 6 TB that don't fit on one machine.
The project brief
You're the engineer in charge of scaling Enlace's database. A single machine no longer suffices: the 6 TB don't fit comfortably and the reads squeeze. The team asks you for a topology proposal. These are the input data, fixed (the anchor numbers of the whole guide):
| Input data | Value | Where it comes from |
|---|---|---|
| Total storage at 5 years | ~6 TB | ~1 KB/record × 6 billion (module 2) |
Reads per second (qps_read) | ~3,858 (~4,000) | 100:1 ratio (module 2) |
Writes per second (qps_write) | ~39 (~40) | module 2 |
| Cache hit ratio | 90% | module 4 project |
| Shard key | short_code | lesson 5 (the dominant read knows it) |
| Spread | by hash, consistent hashing + vnodes | lessons 5-7 |
And these are the decisions you make (the design levers): how much data you put per shard (and therefore how many shards), how many reads you budget per replica (and therefore how many replicas per shard), how much redundancy you add over the arithmetic minimum, how you route reads and writes, and how you handle the lag. The project consists of choosing each one, justifying it, and computing the consequences with code.
The plan steps
Before seeing the reference solution, here's the process. Do it yourself first; the complete solution is after, in a collapsible block, so you can compare.
Step 1 — Decide how many shards for the 6 TB
Sharding resolves the data and writes axis (lesson 5), not the reads one. Choose a data budget per shard —how many TB you want a node to comfortably handle, with margin to grow— and divide the 6 TB by it. Round up and add slack. A power of 2 (4, 8) is convenient for reasoning about the spread. Have at hand why: you shard for the 6 TB, not for the reads.
Step 2 — Spread the load among the shards
With the module 4 cache at 90%, only 10% of the ~4,000 reads/s reaches the database: (1 − 0.90) × 3,858 ≈ 386/s. Spread those reads and the ~40 writes/s among your shards (by hash, they go evenly). Compute the load per shard: reads/s per shard and writes/s per shard primary. You'll see that, after the cache and the sharding, the load per node is tiny.
Step 3 — Decide how many replicas per shard
Replication resolves the reads axis (lesson 3). With a budget of reads/s per replica, compute how many replicas each shard needs for its share of the reads. Then —this is what separates the arithmetic floor from the production design— add redundancy: size to hold up with N−1 replicas, not N (lesson 3). Count the total nodes.
Step 4 — Route, handle the lag, and demonstrate you grow without a storm
Define the routing (writes → the shard's primary; reads → a shard replica) and the lag mitigation (lesson 4: most of Enlace's reads tolerate the lag; for the just-created "read your own write", read from the primary for a moment). And —the climax— run the comparison from lessons 6-7: when you add a shard, mod-N remaps almost everything and consistent hashing only ~K/N. That number is the proof that your plan grows without a massive migration.
Acceptance criteria
Your delivery is complete when the plan answers, and you demonstrate it with the arithmetic and the run output:
- Number of shards justified by the 6 TB and an explicit data budget per shard.
- Load per node computed: reads/s per shard (after cache) and writes/s per primary, with the numbers.
- Replicas per shard justified by the reads budget and by the N−1 redundancy rule.
- Routing defined: where each write and each read goes, and how the lag is handled.
- Growing without a storm, measured: the run output shows
mod-Nremapping ~88.9% and consistent hashing ~13.1% when going from 8 to 9 shards, with the TB moved in each case. - Topology diagram (shards + primary/replicas + cache + routing).
- List of tradeoffs: what you chose, what it cost, and what would change the decision.
Reference solution
It's not the only correct answer —another budget per shard or more replicas for redundancy are also defensible—, but it's a solid proposal with each number justified. Try yours before opening this.
See the complete reference solution (code + run output)
First, the calculator that produces the plan, including the remapping demonstration from lessons 6-7:
# enlace_scaling_plan.py — the scaling plan for Enlace's database.
import bisect
import hashlib
import math
from collections import Counter
# ---------- Enlace's anchor numbers ----------
TOTAL_TB = 6.0 # ~6 TB at 5 years (module 2)
qps_read = 3_858 # ~4,000 reads/s
qps_write = 39 # ~40 writes/s
hit_ratio = 0.90 # module 4 cache
tb_per_shard = 1.0 # DECISION: data budget per shard (comfortable)
reads_per_replica = 2_000 # DECISION: reads/s a replica handles
# ---------- Step 1: how many shards for the 6 TB ----------
shards_min = math.ceil(TOTAL_TB / tb_per_shard)
SHARDS = 8 # DECISION: 8 (power of 2, margin)
print(f"Data: {TOTAL_TB} TB / {tb_per_shard} TB per shard -> minimum {shards_min} shards")
print(f"We choose SHARDS = {SHARDS} (~{TOTAL_TB/SHARDS*1000:.0f} GB per shard, with margin)\n")
# ---------- Step 2: load per shard ----------
reads_to_db = qps_read * (1 - hit_ratio) # the cache absorbs 90%
reads_per_shard = reads_to_db / SHARDS
writes_per_shard = qps_write / SHARDS
print(f"Reads to the DB (after 90% cache): {reads_to_db:,.0f}/s -> {reads_per_shard:,.1f}/s per shard")
print(f"Writes: {qps_write}/s -> {writes_per_shard:.1f}/s per shard primary\n")
# ---------- Step 3: replicas per shard ----------
replicas_needed = max(1, math.ceil(reads_per_shard / reads_per_replica))
replicas_prod = max(2, replicas_needed + 1) # +1 for redundancy (hold up N-1)
total_nodes = SHARDS * (1 + replicas_prod) # 1 primary + R replicas per shard
print(f"Replicas per shard: minimum {replicas_needed}, in production {replicas_prod} (redundancy)")
print(f"Total nodes: {SHARDS} x (1 primary + {replicas_prod} replicas) = {total_nodes}\n")
# ---------- Step 4: growing without a storm (lessons 6-7) ----------
def make_keys(k):
alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
out = []
for i in range(k):
num, s = 100_000_000 + i, ""
while num:
s, num = alphabet[num % 62] + s, num // 62
out.append(s.rjust(7, "0"))
return out
def ring_point(text):
return int(hashlib.md5(text.encode()).hexdigest(), 16) % (2 ** 32)
def node_modn(code, n):
return int(hashlib.md5(code.encode()).hexdigest(), 16) % n
def build_ring(nodes, vnodes=100):
ring = {}
for node in nodes:
for i in range(vnodes):
ring[ring_point(f"{node}#{i}")] = node
return ring
def node_ring(code, ring, pts):
p = ring_point(code)
idx = bisect.bisect(pts, p)
if idx == len(pts):
idx = 0
return ring[pts[idx]]
K, N = 1_000_000, SHARDS
keys = make_keys(K)
# mod-N: remapping when going from N to N+1
moved_modn = sum(node_modn(k, N) != node_modn(k, N + 1) for k in keys)
# consistent hashing: same experiment over the ring
nodes = [f"db-{i}" for i in range(N)]
ring = build_ring(nodes)
pts = sorted(ring)
before = {k: node_ring(k, ring, pts) for k in keys}
for i in range(100): # we add shard 9 (db-8)
ring[ring_point(f"db-{N}#{i}")] = f"db-{N}"
pts = sorted(ring)
moved_ring = sum(node_ring(k, ring, pts) != before[k] for k in keys)
print(f"Adding shard {N+1} (from {N} to {N+1}), {K:,} keys:")
print(f" mod-N remaps {moved_modn:>9,} ({100*moved_modn/K:5.1f}%) ~{moved_modn/K*TOTAL_TB:.2f} TB moved")
print(f" consistent hash remaps {moved_ring:>9,} ({100*moved_ring/K:5.1f}%) ~{moved_ring/K*TOTAL_TB:.2f} TB moved")
print(f" improvement: {moved_modn/moved_ring:.1f}x less data moved\n")
# even spread among the 9 shards after growing (vnodes do their job)
dist = Counter(node_ring(k, ring, pts) for k in keys)
print("Spread among the 9 shards (vnodes=100):", [dist[f"db-{i}"] for i in range(N + 1)])
What to expect. Running python enlace_scaling_plan.py gives, exactly:
Data: 6.0 TB / 1.0 TB per shard -> minimum 6 shards
We choose SHARDS = 8 (~750 GB per shard, with margin)
Reads to the DB (after 90% cache): 386/s -> 48.2/s per shard
Writes: 39/s -> 4.9/s per shard primary
Replicas per shard: minimum 1, in production 2 (redundancy)
Total nodes: 8 x (1 primary + 2 replicas) = 24
Adding shard 9 (from 8 to 9), 1,000,000 keys:
mod-N remaps 888,920 ( 88.9%) ~5.33 TB moved
consistent hash remaps 130,623 ( 13.1%) ~0.78 TB moved
improvement: 6.8x less data moved
Spread among the 9 shards (vnodes=100): [107501, 95552, 117241, 110026, 105966, 111195, 117227, 104669, 130623]
Read the output against the acceptance criteria:
- 8 shards, ~750 GB each. With a budget of 1 TB per shard, the 6 TB call for a minimum of 6; we choose 8 (a power of 2, comfortable to reason about) to leave each shard at ~750 GB, with slack to grow before the next reshard.
- Tiny load per node: after the cache (90%) only 386 reads/s reach the database, which spread among 8 shards are ~48/s per shard; the writes are ~5/s per primary. The cache (M4) and the sharding together leave each node almost idle —that slack is what absorbs the spikes and the mass-miss day—.
- 2 replicas per shard. The arithmetic floor is 1 replica per shard (48/s fits with plenty to spare in the 2,000/s budget), but we deploy 2 to hold up with one crash (N−1 rule, lesson 3): if a replica goes down, the other absorbs its load without sweating. Total: 8 × (1 primary + 2 replicas) = 24 nodes.
- Growing without a storm, measured: adding shard 9 with
mod-Nremaps 888,920 keys (88.9%) ≈ 5.33 TB; with consistent hashing, 130,623 (13.1%) ≈ 0.78 TB —6.8× less data moved—. That's the difference between an hours-long migration that cools the whole cache and a routine operation that only touches the new node's arc. - Even spread after growing: with vnodes=100, the 9 shards end up between ~95k and ~130k keys —reasonably even, without the 10× imbalance the ring without vnodes would have (lesson 7)—. The new shard
db-8has 130,623, which is exactly the number of remapped keys: when adding a node, the only keys that move are the ones that node captures from its arc (no one moves between old nodes).
The topology diagram
flowchart TD
client["Clients"]
cache["cache (Redis)<br/>hit ratio 90%<br/>absorbs 3,472/s"]
router["Routing layer<br/>shard = ring(short_code)"]
client -->|"resolve / shorten"| cache
cache -->|"miss: 386 reads/s + 39 writes/s"| router
subgraph shard0["Shard 0 (~750 GB)"]
p0[("PRIMARY<br/>writes")]
r0a[("replica")]
r0b[("replica")]
p0 -->|replication log| r0a
p0 -->|replication log| r0b
end
subgraph shardN["Shard 7 (~750 GB)"]
pN[("PRIMARY<br/>writes")]
rNa[("replica")]
rNb[("replica")]
pN -->|replication log| rNa
pN -->|replication log| rNb
end
router -->|"write -> primary"| p0
router -->|"read -> replica"| r0a
router -->|"write -> primary"| pN
router -->|"read -> replica"| rNa
note["... shards 1..6 the same<br/>(8 shards x 3 nodes = 24)"]
The capacity table
| Metric | Value | Justification |
|---|---|---|
| Shards | 8 | 6 TB / ~750 GB per shard, with margin (minimum 6, power of 2) |
| Data per shard | ~750 GB | 6 TB / 8, roomy for a node |
| Replicas per shard | 2 | floor 1 + 1 for redundancy (hold up N−1) |
| Total nodes | 24 | 8 × (1 primary + 2 replicas) |
| Reads/s to the DB | 386/s | (1 − 0.90) × 3,858; the cache absorbs 90% |
| Reads/s per shard | ~48/s | 386 / 8, spread by hash |
| Writes/s per primary | ~5/s | 39 / 8 |
| Shard key | short_code | the dominant read knows it (lesson 5) |
| Spread | consistent hashing + vnodes (100) | ~K/N remapping when growing (lesson 7) |
| Growing 8→9 shards | ~0.78 TB moved (13.1%) | vs 5.33 TB (88.9%) with mod-N |
The list of tradeoffs
- Why 8 shards and not 6 (the minimum). 6 shards would fit (1 TB each), but they leave little margin before the next reshard. 8 leaves ~750 GB per shard and is a power of 2. Cost: more nodes to operate. What would change it: if the growth were faster than foreseen, I'd start with more shards; with consistent hashing, adding the 9th is no longer traumatic, so starting tight is less risky than before.
- Why 2 replicas per shard and not 1. The calculation says 1, but 1 doesn't tolerate a crash. Cost: it doubles the read nodes (from 8 to 16 replicas). What would change it: if the cache were even more effective or the SLA laxer, 1 replica + failover to the primary might be enough; for an always-available service, the redundancy is worth its cost.
- Why consistent hashing and not
mod-N.mod-Nspreads just as well with fixed N, but when growing it remaps 88.9% (5.33 TB, total cold cache). Consistent hashing moves ~13.1% (0.78 TB). Cost: a bit more complexity (the ring, the vnodes, a positions table). What would change it: nothing realistic; the ring is the standard choice. Only a system that never changes N could stick withmod-N, and "never grow" isn't a plan. - Why asynchronous replication (lag) and not synchronous. Enlace's data (
short_code → long_url) tolerates a millisecond lag without drama, and asynchronous is faster. Cost: the "read your own write" problem —a just-createdshort_coderesolved from a behind replica gives a fleeting 404—. Mitigation: read from the primary for a moment after creating (lesson 4). What would change it: if Enlace needed strong consistency on the immediate read, synchronous or permanent read-from-primary, at the cost of latency.
Common mistakes
Sharding to resolve a reads problem. What happens: someone hits the read limit and shards, carrying all the complexity of sharding when a replica would have resolved it more simply. Why it happens: "more machines" feels like the answer to any saturation. How to detect it: if your limit is read throughput and the 6 TB and the writes fit on one machine, you don't need sharding —you need replicas—. How to fix it: remember the two axes (lesson 2): replicas for reads, sharding for data and writes. Enlace shards for the 6 TB, not for the 4,000 reads/s (which the cache + replicas already cover). In the plan, the sharding appears because of the storage, and the replicas because of the reads —two separate decisions—.
Sizing the replicas with the arithmetic floor, no redundancy. What happens: the calculation says "1 replica per shard is enough" and exactly 1 is deployed. The day one goes down for maintenance, that shard is left without read capacity (or its whole load falls on the primary, which also serves writes). Why it happens: the arithmetic minimum is confused with the production design. How to detect it: ask yourself "if a replica of this shard goes down, does the shard keep serving reads?". If the answer is no, you have no margin. How to fix it: size for N−1 (lesson 3); at least 2 replicas per shard even though the calculation says 1. The napkin number is the floor, not the goal.
Choosing mod-N "because it spreads evenly" without testing what happens when growing. What happens: the spread is validated with fixed N (~even, looks good) and the decision is called good, without measuring the remapping when adding a node. The day Enlace needs shard 9, the migration moves 88.9% of the data (5.33 TB), cools the whole cache, and risks a latency spike for hours. Why it happens: the good spread is visible immediately and the remapping problem is invisible until you grow (lesson 6). How to detect it: if your sharding plan didn't run the "N to N+1" experiment, you didn't test what matters. How to fix it: evaluate the scheme by two properties —balance (spreads evenly) and stability (remaps little when changing N)—; mod-N passes the first and fails the second, so the plan uses consistent hashing with vnodes.
Exercises
Exercise 1 — Resize for double the data. Enlace grows and at 10 years accumulates ~12 TB. With the same budget of 1 TB per shard and the same cache (90% hit ratio, reads rising to ~8,000/s), recompute: (a) how many shards the storage calls for, (b) the reads/s per shard after the cache, and (c) how many replicas per shard and how many total nodes.
See solution
import math
TOTAL_TB, tb_per_shard = 12.0, 1.0
qps_read, hit_ratio, reads_per_replica = 8_000, 0.90, 2_000
shards = math.ceil(TOTAL_TB / tb_per_shard) # 12 -> we round to 16 (power of 2)
SHARDS = 16
reads_to_db = qps_read * (1 - hit_ratio) # 800/s
per_shard = reads_to_db / SHARDS # 50/s
replicas = max(2, math.ceil(per_shard / reads_per_replica) + 1)
print(SHARDS, f"{per_shard:.1f}/s per shard", f"{replicas} replicas",
SHARDS * (1 + replicas), "nodes")
# 16 50.0/s per shard 2 replicas 48 nodes
- (a) Shards: 12 TB / 1 TB = 12 minimum; we round to 16 (power of 2, margin), ~750 GB per shard as before.
- (b) Reads per shard: the cache leaves
(1 − 0.90) × 8,000 = 800/sto the database, spread among 16 shards it's 50/s per shard —just as tiny as before, because by doubling data and reads we also double the shards—. - (c) Replicas and nodes: 50/s fits in 1 replica, but for redundancy 2 per shard; total 16 × 3 = 48 nodes. The lesson: when scaling proportionally (data and reads go up together, shards too), the load per node stays constant; the system grows "sideways" without any node heating up. And thanks to consistent hashing, going from 8 to 16 shards is done in increments (9, 10, …16), each moving only ~1/N, not all at once.
Exercise 2 — The mass-miss day, with shards. The cache restarts and starts empty: the hit ratio drops to 0 for a few minutes. The full 4,000 reads/s fall on the database. (a) How many reads/s does each of the 8 shards receive? (b) Does it hold up with the budget of 2,000/s per replica and 2 replicas per shard? (c) What mitigation from the plan reduces the blow?
See solution
- (a) Per shard:
4,000 / 8 = 500 reads/sper shard (spread by hash). The sharding already divides the blow among 8: without sharding, a single node would receive the 4,000. - (b) Yes, it holds up. Each shard has 2 replicas with a budget of 2,000/s each = 4,000/s of read capacity per shard; it receives 500/s. Even if one of the shard's replicas went down during the incident, the other alone (2,000/s) absorbs the 500/s with plenty of margin. The N−1 design and the sharding together make the mass miss —which without them would bring down a single DB— a non-event: 500/s per shard is a fraction of the budget.
- (c) Plan mitigations: (1) warm up the cache after a restart before exposing it to traffic, so the hit ratio doesn't start at 0; (2) the capacity slack —each node runs almost idle in the steady state (~48/s) precisely to absorb spikes like this—; and (3) the sharding, which spreads the blow among 8 nodes instead of concentrating it. The cache is the first line; replicas + sharding are the net beneath, sized to survive the day the cache fails.
Exercise 3 — Defend your plan against three objections. A colleague questions your topology (8 shards, 2 replicas per shard, consistent hashing). Answer each objection in a couple of sentences, with numbers. (a) "24 nodes for 40 writes/s and 386 reads/s is a brutal waste." (b) "Why not mod-N, which is one line of code and spreads just as well?" (c) "The short_code as shard key doesn't convince me, why not the long_url?"
See solution
- (a) 24 nodes is a waste → no, it's for the data and the redundancy, not for the throughput. You're right that 40 writes/s and 386 reads/s are trivial loads; the 24 nodes don't come from the throughput but from two things: (1) the 6 TB don't fit on one machine, and splitting them calls for 8 shards; and (2) each shard needs replicas to survive a crash (N−1 redundancy), hence the 2 per shard. The compute capacity is plenty —each node runs almost idle—; what isn't plenty is space (6 TB) or fault tolerance. You could use 6 shards instead of 8 to save, but not much lower without running out of storage or redundancy margin.
- (b)
mod-Ninstead of consistent hashing → it spreads the same with fixed N, but when growing it's a storm. True thatmod-Nis one line and spreads evenly as long as N doesn't change. The problem appears the day you add shard 9: I measured it, it remaps 88.9% (888,920 keys, 5.33 TB) and cools the whole cache, against the 13.1% (0.78 TB) of consistent hashing. Since sharding exists to be able to grow, choosing the scheme that makes growth a catastrophe makes no sense. The ring's extra complexity is paid once; themod-Nstorm is paid on every reshard. - (c)
long_urlas shard key → no, the dominant read doesn't know it. The dominant operation isresolve(short_code) → long_url: ashort_codearrives and thelong_urlhas to be found. If I sharded bylong_url,resolvewouldn't know which shard to go to (it doesn't have thelong_url, it's precisely looking for it), so each resolution would ask all the shards —a cross query, slow, at 386/s—. Theshort_codeis the correct key because the dominant read always brings it in hand, it's very high cardinality (62⁷), and hashed it spreads evenly (lesson 5).
The lesson of this exercise: a scaling plan isn't a stack of machines, it's a set of decisions —how many shards and why, how many replicas and why, what spreading scheme and why— that you know how to defend with the arithmetic and the module's experiments. Each objection is answered by acknowledging what's true and anchoring the decision in a measured number.
Summary and next step
In this project you produced the scaling plan for Enlace's database, the module's capstone. You started from the anchor numbers and decided, with arithmetic and code: 8 shards for the 6 TB (~750 GB each), 2 replicas per shard (floor 1 + N−1 redundancy) for a total of 24 nodes, with the module 4 cache leaving only 386 reads/s to the database (~48/s per shard). You routed writes to each shard's primary and reads to its replicas, handled the lag with asynchronous replication + read-from-primary after creating, and —the climax— ran the proof that you grow without a storm: adding shard 9 with mod-N remaps 888,920 keys (5.33 TB), with consistent hashing only 130,623 (0.78 TB), 6.8× less. You delivered the topology diagram, the capacity table, and the list of tradeoffs with their conditions —which turns "we added more machines" into an engineering design—.
Before moving on you should be able to: justify the number of shards by the storage and that of replicas by the reads and the redundancy; route each operation and handle the lag; put the two remapping numbers (88.9% vs 13.1%) side by side and translate them into TB moved; and defend each choice with its number and its condition.
With this you close module 5. Enlace's database now scales on both axes —replicas for the reads, sharding with consistent hashing for the data and the writes—, and it grows without traumatic migrations. But a system with 24 database nodes, a cache, and several application servers needs something in front that spreads the traffic among those servers and survives one of them going down. In module 6 you'll see load balancing (round-robin, least-connections, by hash) and statelessness —why Enlace's servers must not keep local state so they can scale horizontally and replace themselves without drama—, with the health checks that take the sick node out of rotation. Scaling the database was this module; scaling the application layer that queries it is next.
Resources
- Designing Data-Intensive Applications, Martin Kleppmann — Chapters 5 (Replication) and 6 (Partitioning) — the reference treatment of everything you planned here: primary/replica and lag (ch. 5), hash-of-key sharding, shard-key choice, hotspots, and consistent hashing (ch. 6). Ideal for reviewing the whole module before moving on to balancing.
- PostgreSQL — Streaming Replication — how a real engine implements streaming the primary's replication log to the replicas; the concrete grounding of your plan's read replicas, with its synchronous/asynchronous distinction that governs the lag.
- Amazon Dynamo paper (2007) — section 4.2, "Partitioning Algorithm" — how a production system uses consistent hashing with virtual nodes for exactly your plan's reasons; the bridge toward how replication is done on the ring, a topic of the sibling resilience guides.
- System Design Primer — Sharding, Replication and Load balancing — a practical summary of the three tools —the two you used here and module 6's balancing— in the context of a complete system design, useful for seeing how the scaling pieces fit together.