Module 1: How to Approach a System Design Problem

8. Mini-project: from a vague prompt to requirements and scope

Description

This lesson is your graduation from module 1. You'll put together everything you learned —understand before you draw, functional vs. non-functional, the clarifying questions, scoping, the 4-step framework, the single box, and the tradeoffs— into a single deliverable: a system's starter package. It's what a good designer produces in the first thirty minutes facing a vague prompt, before touching the implementation: the document that turns "design a URL shortener" into a defensible plan with numbers, a diagram, and a list of risks.

The deliverable has six parts, and all six matter: (1) the requirements —functional and non-functional, the latter with numbers—; (2) the clarifying questions, with the stated assumptions where there's no answer; (3) the in/out scope table, with the reason for each deferral; (4) the estimation, computed (not quoted); (5) the single-box diagram, with its two paths; and (6) the three points where that design will break, each mapped to its solution. None requires writing a line of product: it's pure design work, the kind that decides whether the system is born well or poorly aimed.

Connection to the module: here the arc closes. Lessons 1 to 5 gave you the method (understand, ask, scope, the framework); lessons 6 and 7, the first design and how to reason about it. This lesson puts you to work producing the complete artifact with your own hands, from start to finish, so that you enter module 2 —where estimation becomes a full discipline— knowing exactly what step 1 produces and why step 2 (the back-of-the-envelope math) is so central. The worked solution applies everything to Enlace, the case you already know; the exercises transfer it to a new system, so you can confirm you learned a method, not a memorized answer.

The architect who delivers the first plan

Think of it this way. You hired an architect for your house (the analogy from lesson 1, now complete). After the first meeting —where they asked you all the questions—, they don't come back with the house built. They come back with a starter folder: the summary of your needs (how many rooms, budget, lot), the list of decisions still to be resolved with their assumptions ("I assume three bedrooms; confirm it"), what this first version includes and what's left for a future expansion ("the pool goes in phase 2"), an estimate of costs and materials, a sketch of the floor plan, and an honest note of the risks ("the sloped lot will make the foundations more expensive"). That folder isn't the house. It's the plan of the house: enough for you, the client, to say "yes, let's go this way" or "no, change this" —before spending a peso on cement—.

Notice the value of that folder. It fits in a few sheets, it's produced in hours, and yet it decides the fate of the project: if the requirements are misunderstood or the scope poorly bounded, no amount of good construction fixes it —you'd build the wrong house perfectly—. The starter folder is cheap to make and very expensive to omit. An architect who skips this step and goes straight to building is a dangerous architect, however good their bricklaying.

A system's starter package is that folder. It's what you deliver before writing code: understood requirements, bounded scope, estimated numbers, sketched design, named risks. Producing it well is the difference between building the correct system and perfectly building the wrong one. In this mini-project, you produce Enlace's starter package from start to finish —and then that of a new system, to prove you know how to do it yourself, not just copy it—.

Worth the sentence that guides the project:

Before building anything, you deliver the starter package: requirements with numbers, bounded scope, computed estimation, simple diagram, and named risks. It's cheap to make, very expensive to omit, and decides whether the correct system is born.

What you'll build

The object of study is Enlace, which you already know inside out. Your mission: produce its complete starter package, the six parts, following the 4-step framework. These are the steps; follow them in order, because each rests on the previous one.

Step 1 — Requirements (functional and non-functional)

Separate what the system does from how well it does it, and put a number on each non-functional.

Step 2 — Clarifying questions and assumptions

List the questions you'd ask the owner, and for the ones that have no answer at hand, state the assumption (number, justification, and what changes if it's false).

Step 3 — In/out scope table

Decide what goes into v1 and what's deferred, with the reason for each deferral. Watch for features that change the class of the system.

Step 4 — Computed estimation

Run the back-of-the-envelope math: read and write QPS, storage, code space. Compute, don't quote.

Step 5 — Single-box diagram

Draw the server + the DB, with the write path and the read path.

Step 6 — The three breaking points

Name where the single box will break and which solution (module) each crack sends you to.

The complete solution

Here is Enlace's finished starter package, so you can compare with yours. Build yours first; reading the solution without having tried is like reading the plan of a house you never imagined.

Part 1 — Requirements

FUNCTIONAL (what it does)
  F1. shorten(long_url) -> short_code   (7 chars base62, unique)
  F2. resolve(short_code) -> 301/302 redirect to long_url
  F3. If the short_code doesn't exist -> 404
  F4. (optional, deferred) link expiration
  F5. (optional, deferred) click counting (analytics)

NON-FUNCTIONAL (how well) -- each with its number
  N1. Write throughput: ~40 ops/s
  N2. Read throughput:  ~4000 ops/s  (100:1 ratio, READ-HEAVY)
  N3. Redirect latency: < 100 ms
  N4. Availability:     99.9%  (<= 8.76 h down/year)
  N5. Durability:       never lose links
  N6. Consistency:      eventual (a new link can take ~1s)

Part 2 — Clarifying questions and assumptions

QUESTIONS (by family):
  1. Scale:     how many new URLs/month? read:write ratio? peaks?
  2. Ops:       just shorten and resolve, or also edit/delete?
  3. Data:      how long does a link live? URL size?
  4. Quality:   target latency? availability nines? consistency?
  5. What NOT:  analytics in v1? custom URLs? user accounts?

STATED ASSUMPTIONS (where there's no answer):
  S1. 100M new URLs/month.
      Why: scale of a large public shortener.
      If I'm wrong: at 1M/month one box would be plenty; at 10,000M/month it
                    would need much more sharding.
  S2. read:write ratio = 100:1 (read-heavy).
      Why: a link is visited much more than it's created.
      If I'm wrong: at 1:1, the cache would pay off much less.
  S3. ~1 KB per record, 5-year retention.
      Why: URL ~500B + metadata + index overhead.

Part 3 — In/out scope table

In v1Out (deferred)Why
shortenClick analyticsMultiplies the write ×100 (~40→~4000/s)
resolve (301/302)Custom URLsNice, not essential; separate validation
404ExpirationAdded later without redoing the core
Accounts, rate limits, anti-spamSystems apart, outside the heart

Part 4 — Computed estimation

# Enlace's starter package: the estimation (step 2 of the framework)
writes_per_month = 100_000_000
ratio = 100
bytes_per_record = 1024
years = 5

seconds_per_month = 30 * 24 * 3600
qps_write = writes_per_month / seconds_per_month
qps_read = qps_write * ratio
records = writes_per_month * 12 * years
storage_tb = records * bytes_per_record / 1e12
code_space = 62 ** 7
fraction_used = records / code_space * 100

print(f"writes/s        = {qps_write:8.1f}  (~40)")
print(f"reads/s         = {qps_read:8.1f}  (~4000, READ-HEAVY)")
print(f"records 5y      = {records:>14,}")
print(f"storage         = {storage_tb:8.2f} TB  (~6 TB)")
print(f"code space      = {code_space:>14,}  (62^7)")
print(f"fraction used   = {fraction_used:8.3f} %  (plenty)")

What to expect. Running this with Python 3.14.0:

writes/s        =     38.6  (~40)
reads/s         =   3858.0  (~4000, READ-HEAVY)
records 5y      =  6,000,000,000
storage         =     6.14 TB  (~6 TB)
code space      =  3,521,614,606,208  (62^7)
fraction used   =    0.170 %  (plenty)

Part 5 — Single-box diagram

graph LR
    C[Client / Browser] -->|HTTP| S[Enlace server<br/>shorten / resolve]
    S -->|SQL| DB[(DB: links table<br/>short_code PK indexed)]

Write path (shorten): client sends long_url → server generates short_code (7 chars base62) → INSERT into links → returns the code. One write per shortening (~40/s, trivial).

Read path (resolve): browser requests /short_code → server does SELECT ... WHERE short_code = ? (primary-key index lookup, fast) → responds 302 Location: long_url → the browser follows the redirect. One read per resolution (~4000/s, the hot path).

Part 6 — The three breaking points

1. READS SATURATE (~4000/s no margin for spikes)
   -> M4: cache.  Tradeoff: speed in exchange for stale data.

2. DISK FILLS (1.23 TB/year; 6 TB at 5 years)
   -> M5: replicas + sharding.  Tradeoff: scale in exchange for lag and complexity.

3. SINGLE POINT OF FAILURE (one box doesn't meet 99.9%)
   -> M7: redundancy + failover.  Tradeoff: availability in exchange for
      consistency (CAP).

(Fourth, latent: a single server's compute -> M6 balancing/stateless
 when read spikes exceed what one machine sustains.)

That's Enlace's complete starter package: six parts, it fits in two screens, and it contains everything needed for someone to say "yes, let's go this way" —or to start modules 2 to 7 with the direction set—. Notice that each number is computed, each assumption stated, each deferral justified, and each crack mapped. That's what distinguishes a professional starter folder from a random box drawing.

Common mistakes

Delivering the diagram without the numbers. What happens: someone produces a nice single-box diagram but omits the estimation (part 4), so nobody —not even they— knows whether that box handles the problem. The diagram is decoration without the numbers that justify it. Why it happens: drawing is satisfying, computing is work. How to detect it: if your package has a diagram but no estimation section with QPS and storage, it's lame. How to fix it: the diagram and the numbers go together, always. A design without estimated numbers isn't a design, it's a drawing.

Assumptions without the "if I'm wrong" part. What happens: someone states "I assume 100M/month" and stops there, without saying why it's reasonable or what would change if it were another number. The assumption exists but is useless: nobody can evaluate it or knows what to recompute if it's wrong. Why it happens: writing the consequence requires thinking through the alternative design, and that costs. How to detect it: if your assumptions don't say "if I'm wrong, the design changes like this", they're half-done. How to fix it: the three parts are mandatory —number, justification, consequence—. The third is what turns it into a design tool and not a guess.

Forgetting the breaking points (delivering the design as if it were final). What happens: someone delivers the single box as "Enlace's design" without naming where it breaks, implying it's the complete solution. When the system grows, nobody anticipated the cracks and fires get put out. Why it happens: naming the limits of what you just designed feels like admitting it's incomplete. It's the reverse: naming the cracks is the most mature part of the design. How to detect it: if your package doesn't have a "where this will break" section, it's missing the part that proves you understand your own design. How to fix it: part 6 —the breaking points mapped to solutions— is mandatory. An honest design comes with the map of its own limits.

Exercises

The exercises ask you to produce the starter package of a new system, to confirm you learned the method and didn't memorize Enlace. The prompt, with its numbers:

Pega — a pastebin service. People paste a snippet of text (code, notes) and get a short link to share it; whoever opens the link sees the text. Business data: 5 million new pastes a month, each paste weighs ~10 KB, each paste is read ~20 times on average, retention 2 years.

Exercise 1 — Pega's requirements, questions, and scope. Produce parts 1, 2, and 3 of Pega's starter package: the functional and non-functional requirements (mark which non-functionals you can already put as a number with the given data and which you'd have to ask/assume), at least two clarifying questions with one stated assumption, and the in/out scope table with at least two deferred features.

See solution

Requirements:

FUNCTIONAL
  F1. create_paste(content) -> paste_id
  F2. get_paste(paste_id) -> the text
  F3. 404 if the paste_id doesn't exist

NON-FUNCTIONAL (with the given data)
  N1. Write: ~2 ops/s   (5M/month, we compute it in ex. 2)
  N2. Read:  ~40 ops/s   (20:1 ratio)
  N3. Size per record: ~10 KB
  N4. Retention: 2 years
NON-FUNCTIONAL (to ask/assume)
  N5. Target latency (not given) -> assume < 200 ms
  N6. Availability (not given)    -> assume 99.9%
  N7. Consistency (not given)     -> assume eventual (a new paste can
                                     take ~1s to propagate)

Questions and assumptions:

QUESTIONS: do pastes expire or live forever within the 2 years?
           are there private/password-protected pastes? max paste size?
           what availability is expected?

ASSUMPTION: latency < 200 ms to see a paste.
  Why: it's text, not an instant redirect; 200 ms feels fast
       for loading a text page.
  If I'm wrong (< 50 ms): would need to serve from a more aggressive cache/CDN.

In/out scope:

In v1Out (deferred)Why
create_pastePassword-protected pastesSeparate auth layer; not core
get_pasteSelf-destruct on readChanges the data lifecycle; deferred carefully
404Formatting/syntax highlightingCosmetic, pure accessory

Notice the parallel with Enlace (same skeleton: one write, one read, one 404) and the differences the domain imposes: the record is 10 times bigger (10 KB vs. 1 KB), the ratio is gentler (20:1 vs. 100:1), and a domain-specific trap feature appears ("self-destruct on read", which changes the lifecycle). You learned the method if you knew how to transfer the skeleton and detect what changes.

Exercise 2 — Pega's computed estimation. Write and "run" (by hand or in Python) Pega's estimation: writes/s, reads/s (20:1 ratio), storage at 2 years, and —extra— decide how many base62 characters the paste_id needs for the records at 2 years without falling short. Round to orders of magnitude.

See solution
writes_per_month = 5_000_000
ratio = 20
bytes_per_record = 10_000       # 10 KB
years = 2

seconds_per_month = 30 * 24 * 3600
qps_write = writes_per_month / seconds_per_month
qps_read = qps_write * ratio
records = writes_per_month * 12 * years
storage_tb = records * bytes_per_record / 1e12

print(f"writes/s   = {qps_write:.2f}   (~2/s)")
print(f"reads/s    = {qps_read:.0f}    (~40/s)")
print(f"records 2y = {records:,}")
print(f"storage    = {storage_tb:.2f} TB  (~1.2 TB)")
for n in (5, 6, 7):
    print(f"  62^{n} = {62**n:,}  uses {records/62**n*100:.3f}% of the space")

Output (Python 3.14.0):

writes/s   = 1.93   (~2/s)
reads/s    = 39     (~40/s)
records 2y = 120,000,000
storage    = 1.20 TB  (~1.2 TB)
  62^5 = 916,132,832  uses 13.099% of the space
  62^6 = 56,800,235,584  uses 0.211% of the space
  62^7 = 3,521,614,606,208  uses 0.003% of the space

Interpretation: Pega is much calmer than Enlace —~2 writes/s and ~40 reads/s, a hundred times less load— because its scale (5M/month, 20:1 ratio) is much smaller. With 120M records at 2 years, a 6-character base62 paste_id is plenty (uses only 0.21% of the space); 5 characters would be risky (13% used and climbing, it starts to get tight); 7 would be wasteful. Notice how the method gives a different result from Enlace's (which needed 7 chars) precisely because the numbers are different —that's designing with numbers, not reciting—. The storage, 1.2 TB at 2 years, fits comfortably on a single machine.

Exercise 3 — Pega's single box and breaking points. Complete Pega's package: draw (ASCII or mermaid) its single box with the two paths, and name its breaking points. With the numbers from exercise 2, answer: does Pega already need a cache, replicas, or balancing, or is a single box more than enough? Justify with the numbers, and say which would be the first limit to break if Pega grew 100 times.

See solution

Pega's single box:

Client --HTTP--> Pega server --SQL--> DB (pastes table, paste_id PK indexed)
                 (create / get)
  • Write (create_paste): receives text → generates paste_id (6 chars base62) → INSERT → returns the id. ~2/s: trivial.
  • Read (get_paste): receives id → SELECT content WHERE paste_id = ? (PK index) → returns the text. ~40/s: trivial.

Does it need a cache/replicas/balancing now? No. With ~2 writes/s, ~40 reads/s, and 1.2 TB at 2 years, a single box is more than enough by a wide margin: 40 reads/s any indexed DB does without breaking a sweat, and 1.2 TB fits on a single disk. Adding a cache, replicas, or balancing here would be textbook over-engineering —the number doesn't demand it—. This is the intranet-Enlace of lesson 2: same functional requirements as a large system, but at a scale that collapses the design into one box. The discipline of "the simplest design that meets wins" says: stay in the single box.

Breaking points (if Pega grew):

If Pega x100 (500M pastes/month):
  1. READS: ~40/s -> ~4000/s.  FIRST limit to break. -> M4 cache.
  2. STORAGE: 1.2 TB -> ~120 TB at 2 years.  -> M5 sharding.
  3. SINGLE POINT OF FAILURE: if the required availability rises. -> M7.

The first limit to break on growing x100 would be the reads (~40 → ~4000/s), just like in Enlace —it's a (more) read-heavy system—, and it would send you to the cache (M4). Notice the conclusion of the whole module: the same method, applied to Pega, says "one box is enough today" —the opposite of what it would demand at more scale—, and that capacity for the design to respond to the numbers instead of applying a fixed recipe is, exactly, what you learned to do in module 1.

Summary and next step

In this mini-project you produced, from start to finish, a system's starter package: requirements with numbers, questions and assumptions, scope table, computed estimation, single-box diagram, and the breaking points mapped to solutions. It's the folder the architect delivers before laying a brick —cheap to make, very expensive to omit, and decisive for the correct system to be born—. You did it for Enlace (the known case) and transferred it to Pega (a new one), confirming you learned a method that responds to the numbers, not a memorized answer: that's why Enlace called for 7 chars and a future cache, and Pega settled for 6 chars and a single box.

With this you close module 1 and the phase of how you approach a design problem. You now know how to take a vague prompt and turn it into a defensible plan: understand before you draw, separate functional from non-functional, ask and assume, scope, apply the 4-step framework, draw the simple box, and reason about its tradeoffs and its cracks.

Before moving on you should be able to: produce the complete starter package (the six parts) for any vague prompt; state assumptions with their three parts; compute a back-of-the-envelope estimation and read what each number tells you; and decide, with the numbers, whether a system already needs complexity or a single box is enough.

What comes next is going deeper into the step that turned out to be the heart of the method: estimation. In module 1 we ran it as a preview; module 2 turns it into a rigorous discipline —powers of 10, seconds per day, units (KB/MB/GB/TB), read and write QPS, storage over N years, bandwidth, and working-set memory—, all with the Enlace numbers you already know. There the back-of-the-envelope math, which here was a tool, becomes a superpower.

Resources