Module 8: Project — Design Enlace End to End
2. Step 1 — Requirements and scope for Enlace
Description
The journey back begins, and it begins where every design begins: step 1 of the framework, the requirements. Even though you have spent eight modules with Enlace and could recite it in your sleep, this step is not a formality of memory: it is explicitly declaring the three lists that are the foundation of the entire design —the functional requirements (what Enlace does), the non-functional ones (how well, with a number), and the scope table (what enters the v1, what is deferred)—. Everything you draw afterward is justified against these lists. If a box of the diagram does not answer a requirement, it is superfluous; if a requirement has no box, it is missing. Step 1 is the yardstick against which everything else is measured.
In this lesson you produce Enlace's complete step 1, in the exact format that goes into the final deliverable. You will see that the functional ones fit in three lines, that the non-functional ones are almost all numbers (the same anchor numbers you will estimate in lesson 3), and that the scope table is where the quietest and most powerful design decision is made: what NOT to build. You will understand why declaring "analytics is out of the v1" is not laziness, but the decision that keeps Enlace teachable and cheap —and why the clicks field is the trap that, if you let it in without thinking, multiplies the writes by a hundred—.
Connection to the module: this is the first of the six lessons that walk the design step by step, and it is the base of all of them. Lesson 3 (step 2) takes the non-functional requirements from here and turns them into the capacity table. Lesson 4 (step 3) draws the boxes that satisfy these requirements at that scale. And lessons 5–7 (step 4) deep-dive into the components that these requirements —read through the numbers— mark as critical. Without an explicit step 1, the rest of the capstone floats without a foundation. Here you set it.
The contract before the construction
Think of it this way. Before a construction company lays a single brick, there is a document that governs everything: the construction contract. It is not the construction —it has no bricks or blueprints yet—, but it is what makes the construction possible, because it sets three things unambiguously. First, what is going to be built: a two-story house, three bedrooms, a garage (the functions). Second, with what quality and under what constraints: that it withstands a grade-7 earthquake, that it is delivered in eight months, that it does not cost more than such an amount (the non-functional requirements, almost all with a number). And third —the most forgotten and the most important—, what it does NOT include: the pool goes separately, the garden is the client's, the furniture is not included (the scope). That "what it does not include" is what avoids the most common disaster of any construction: the client who assumed the pool was included, and the builder who assumed it was not.
Notice why the contract goes before the blueprints, and not the other way around. If the architect draws the blueprints first and the contract afterward, they draw blind: they do not know if it is three bedrooms or six, if the budget holds the marble, if the pool is included. The blueprints that come out of that ignorance will have to be redone as soon as the contract appears. The correct order is contract → blueprints → construction, because each one needs what the previous one set. And the contract has an extra virtue: it is the yardstick for resolving disputes. When halfway through construction someone asks "and the pool?", there is no arguing; you look at the contract. It said there, in black and white, that the pool went separately.
The requirements of a system are that construction contract. The functional ones are "what is built" (shorten, resolve). The non-functional ones are "with what quality and constraints" (~4,000 reads/s, latency <100 ms, 99.9% availability). And the scope table is the "what it does NOT include" (analytics goes separately, custom URLs are not in the v1). Just like in the construction, this contract goes before the blueprints (the diagram of lesson 4), because the blueprints you draw have to satisfy it, and it is the yardstick for resolving every design dispute: when you doubt whether a box should be there, you check it against the requirements. If it does not answer any, it is superfluous.
It is worth spelling it out:
Requirements are the contract of the system, and they go before the design. They set three things unambiguously: what it does (functional), how well and under what constraints (non-functional, with a number), and what is left out (scope). The subsequent design is measured against this contract: each component answers a requirement, or it is superfluous.
The functional requirements: what Enlace does
The functional ones are the capabilities of the system: the things it does, written as operations with input and output. For Enlace they fit in three lines, and you have known them since module 1. The discipline here is to write them as a contract, with names in English (the tech market) and explicit behavior on error:
F1 shorten(long_url) -> short_code
Receives a long URL, returns a 7-character base62 short_code.
The public URL is enla.ce/<short_code>.
F2 resolve(short_code) -> redirect
Receives a short_code, returns a 301/302 redirect to the long_url.
F3 A nonexistent short_code -> 404 Not Found.
The behavior on error is part of the resolve contract.
Three functional requirements, and not one more in the v1. Notice two decisions that are already made here. The first is the 301 vs 302: the redirect can be permanent (301, which the browser caches —super fast, but you lose the click count because it stops passing through Enlace—) or temporary (302, which is not cached —every visit passes through Enlace, you can count clicks—). It is your first tradeoff, and it is born from a functional requirement, not from choosing technology. For Enlace's v1 we choose 302, because it leaves the door open to future analytics without redesigning the contract. The second decision is the 404 as part of the contract: resolve of a code that does not exist is not a forgettable edge case; it is specified behavior (F3), and as you saw in module 1, it costs a query to the database even though it returns nothing —a detail that will reappear when sizing the cache—.
What is notable about Enlace's functional requirements is how short they are. That brevity is a feature, not a shortcoming: large systems are almost never complicated in what they do; they are complicated in how well they do it at scale. Enlace demonstrates it by the book —two trivial operations that, when asked for 4,000 reads/s and 6 TB, activate all the machinery of modules 2 to 7—. The functional requirements say what; the difficulty lives in the non-functional ones.
The non-functional requirements: how well (with a number)
Here is the heart of step 1, because the non-functional ones are almost all numbers, and those numbers are the direct input of step 2. A non-functional requirement without a number is a wish ("make it fast"); with a number it is a verifiable requirement ("resolve latency < 100 ms at p99"). The rule: every non-functional requirement carries its figure.
These are Enlace's, derived from the starting assumptions (the data that in a real problem you would extract from the client with questions):
| Non-functional requirement | Value | Where it comes from |
|---|---|---|
| Write scale | ~40 writes/s | 100M URLs/month ÷ seconds in the month (M2) |
| Read scale | ~4,000 reads/s | 100:1 ratio — read-heavy (M2) |
| Storage | ~6 TB at 5 years | 100M/month × 12 × 5 × ~1 KB (M2) |
| Latency | resolve < 100 ms | experience requirement (instant redirect) |
| Availability | 99.9% | max. ~8.76 h of downtime per year |
| Durability | high | a created short_code is never lost |
| Consistency | eventual OK for resolve | the data is almost immutable; a fleeting 404 is tolerated |
Read the table for what it is: Enlace's non-functional profile, and therefore the mold of its design. Each row speaks to the design that is coming:
- ~40 writes/s says: the write path is calm; a single database (one primary) absorbs it without breaking a sweat. No need to deep-dive there.
- ~4,000 reads/s says: the read path is the bottleneck; here go the cache (M4) and the load balancing (M6). This number, alone, marks where the design effort goes.
- ~6 TB says: too much for a machine to be comfortable forever; replicas and sharding (M5).
- Latency <100 ms says: the read has to be almost instant, which reinforces the cache (a 1 ms hit versus a 50 ms miss) and the index on
short_code. - 99.9% availability says: a single box is not enough (a machine fails more than 8.76 h/year); redundancy and failover are needed (M7).
- Eventual consistency says: I can distribute the reads among replicas that are behind without drama, because the
short_code → long_urlmapping does not change and a freshly createdshort_codetolerates a 404 of milliseconds while the replica catches up. This tolerance is what buys the read scalability —it is the star tradeoff of module 7, and here it is declared as a requirement—.
Notice the causal order: each non-functional number is the reason for a future component. When in lesson 4 you draw the cache, it will not be "because big systems have it"; it will be because the row "~4,000 reads/s + latency <100 ms" requires it. Step 1 does not describe the system for the sake of describing it; it prepares the justifications the design is going to need.
The scope table: the decision of what NOT to build
The third deliverable of step 1 is the quietest and the most powerful: deciding what enters the v1 and what is deferred. Scoping is not cutting out of laziness; it is the discipline that keeps the design focused on its heart —shorten, resolve, redirect at scale— and defers everything else until a requirement asks for it. A real industrial shortener would have authentication, rate limits, spam detection, an analytics dashboard, billing. Enlace stays with the heart, on purpose, because the heart is what teaches the fundamentals.
| Capability | v1? | Why |
|---|---|---|
shorten / resolve / 404 | In | The heart: without this there is no Enlace |
| 302 redirect | In | Leaves the door open to future analytics |
Click analytics (clicks) | Out | Multiplies the writes by ~100 (see below) |
Custom URLs (enla.ce/my-brand) | Out | Changes ID generation; feature, not fundamental |
Expiration (expires_at) | Out | Optional feature; the field exists but is not used in v1 |
| Authentication, rate-limiting, anti-spam | Out | Border with other guides; not scale fundamentals |
The row that teaches the most is the click analytics, because it is the classic scope trap. The Link record has a clicks field, and it seems innocent to increment it on every resolve. But let us do the calculation that reveals the danger: Enlace has ~4,000 reads/s. If each read increments clicks, each read becomes also a write. The writes would go from ~40/s to ~4,040/s —a hundred times more—, and suddenly the write path, which was the calm one, becomes the bottleneck. An apparently minor scope decision ("do we count clicks?") changes the entire system: it turns Enlace from read-heavy to read-and-write-heavy, and forces a different design (in-memory aggregation, write queues —which is already territory of the events guide—). That is why the v1 defers analytics: not because it does not matter, but because its correct design is another problem, and throwing it in without thinking breaks the estimation of everything else.
That is the lesson of the scope table: what you leave out shapes the system as much as what you leave in. Scoping well is what allows the rest of the capstone to have the clean numbers it will estimate in the next lesson.
Worked example: the complete step 1, as it goes into the deliverable
Let us put the three lists together into the exact block that opens Enlace's design. This is the first artifact of the capstone, and this compact is how it looks:
=== STEP 1 — ENLACE REQUIREMENTS ===
FUNCTIONAL
F1 shorten(long_url) -> short_code (base62, 7 chars; enla.ce/<code>)
F2 resolve(short_code) -> 302 Location: long_url
F3 nonexistent short_code -> 404
NON-FUNCTIONAL (with number)
writes ~40/s (100M/month)
reads ~4,000/s (100:1 ratio, READ-HEAVY)
storage ~6 TB (5 years, ~1 KB/record)
latency resolve < 100 ms
availability 99.9% (<= 8.76 h downtime/year)
durability high (a created code is never lost)
consistency eventual OK for resolve
SCOPE v1
IN: shorten, resolve, 404, 302 redirect
OUT: click analytics, custom URLs, expiration,
auth, rate-limiting, anti-spam
What to expect. That block of text is the entire step 1, and notice three properties that make it a good foundation. First: it fits on one screen. A step 1 that does not fit on one screen is usually over-specified (you threw in features that are deferred scope) or badly scoped. Second: almost every non-functional requirement has a number —and those numbers are, exactly, the ones lesson 3 is going to execute—. Third: the scope is explicit in both directions —what enters and what leaves—, so that nobody assumes the pool. With this block written, you have Enlace's construction contract. The blueprints come afterward, and they will be measured against this.
Common mistakes
Writing non-functional requirements without a number. What happens: someone lists "the system must be fast, scalable and reliable" and considers step 1 done. But "fast" cannot be designed or verified: 10 ms or 500 ms? 4,000 reads/s or 4 million? Without a figure, the requirement says nothing to step 2, and the design floats. Why it happens: quality adjectives sound like a requirement and are free to write; numbers cost thought. How to spot it: if a non-functional requirement of yours has no figure or unit, it is a wish, not a requirement. How to fix it: give each non-functional requirement its number: "fast" → "resolve < 100 ms"; "scalable" → "~4,000 reads/s"; "reliable" → "99.9% availability". The number is what turns the wish into something step 2 can estimate and the design can satisfy.
Skipping the scope table (assuming "everything is in"). What happens: someone lists the functional and non-functional requirements but does not declare what is left out, and in the design there appears —without discussion— an analytics dashboard, custom URLs and an account system. The system bloats, the numbers get dirty (analytics shoots the writes to 4,040/s) and the design loses focus. Why it happens: "complete design" is confused with "design of everything". How to spot it: if your step 1 has no explicit "OUT" list, you did not scope. How to fix it: always write both columns —in and out— and, for each thing you leave out, the reason. In Enlace, leaving out analytics is not an oversight; it is the decision that keeps the writes at ~40/s and the rest of the estimation clean.
Treating the 404 as an edge case and not as a contract. What happens: someone specifies shorten and resolve but forgets to say what happens with a nonexistent short_code, and in the design that case is left undefined —does it return empty? a 500? a 404?—. Worse: they do not size that a failed resolve also costs a query to the database, which opens the door to an attack of made-up codes that spends reads. Why it happens: the happy path is the one thought about; the error is taken for granted. How to spot it: if your resolve contract does not say what it returns for a code that does not exist, it is incomplete. How to fix it: the behavior on error is part of the functional contract (F3: nonexistent code → 404), and its cost (a query to the DB) is part of the estimation. A functional requirement that only describes success describes half a system.
Exercises
Exercise 1 — Classify each requirement. For each sentence about Enlace, say whether it is a functional requirement, a non-functional one, or a scope decision, and why in one sentence. (a) "resolve of a valid code redirects with a 302." (b) "The p99 of resolve is below 100 ms." (c) "Click analytics is not in the v1." (d) "Enlace holds ~4,000 reads per second." (e) "A short_code that does not exist returns 404."
See solution
- (a) Functional. It describes a capability (the redirect is something Enlace does). It is F2.
- (b) Non-functional. It is not a new capability, it is how fast it fulfills one it already has. Latency requirement, with a number (100 ms, p99).
- (c) Scope. It decides what is NOT built in the v1. Neither functional nor non-functional: it is a decision of what is left out.
- (d) Non-functional. It is how much load it holds, with a number. Scale/throughput requirement (~4,000 reads/s).
- (e) Functional. It is behavior (what it does with a nonexistent code). Part of the
resolvecontract: F3.
The lesson: the functional ones usually start with a verb ("redirects", "returns"); the non-functional ones carry a number or a quality adjective ("below 100 ms", "holds X"); and the scope decisions say what enters or leaves ("not in the v1"). Distinguishing them is what makes step 1 a foundation and not a jumbled list.
Exercise 2 — The hidden cost of a scope requirement. The product team proposes putting click analytics in the v1 —incrementing clicks on every resolve—. Without designing anything yet, compute the impact on the writes: (a) how many writes/s would Enlace have with analytics, starting from ~40 writes/s of shorten and ~4,000 reads/s? (b) By what factor do the writes grow? (c) Why does this change the design, and which sibling guide does the problem push toward?
See solution
- (a) Each
resolve(4,000/s) would also become a write (incrementclicks). The writes would go from ~40/s (onlyshorten) to40 + 4,000 = ~4,040 writes/s. - (b) From ~40 to ~4,040 is a factor of ~100×. Analytics multiplies the writes by a hundred, exactly the read:write ratio, because it turns each read into a write.
- (c) It changes the design at its root: Enlace stops being read-heavy (calm writes, ~40/s) and becomes read-and-write-heavy. The write path, which a single database absorbed without breaking a sweat, is now a bottleneck: incrementing a counter 4,000 times/s on the same row creates contention, and it has to be redesigned (in-memory aggregation and periodic flush, or queuing the click events and processing them separately). That design —counters at scale, asynchronous event processing— is territory of
event-driven-architecture-guide. That is why Enlace's v1 defers analytics: not because it does not matter, but because its correct design is another problem, from another guide, and throwing it in without thinking dirties the estimation of everything else. The scope table is not bureaucracy; it is what protects the cleanliness of the design.
Exercise 3 — The step 1 of a sibling system. Apply only step 1 (the three artifacts: functional, non-functional with a number, scope table) to this prompt: "Design a 'like' service for posts: people like a post, remove their like, and anyone sees the total like count of a post." Assume 500 million posts, an average of 50 new likes per second across the entire system, and that the count is queried ~1,000 times/s. Do not estimate in depth (that is step 2); just declare the contract.
See solution
=== STEP 1 — 'LIKE' SERVICE ===
FUNCTIONAL
F1 like(user_id, post_id) -> registers a like
F2 unlike(user_id, post_id) -> removes the like
F3 count_likes(post_id) -> returns the total number of likes
F4 a user cannot like the same post twice
NON-FUNCTIONAL (with number)
writes ~50/s (like + unlike)
reads ~1,000/s (count_likes; ratio ~20:1, read-heavy)
posts 500M
latency count_likes < 100 ms
availability 99.9%
consistency eventual OK for the count (a like can take
seconds to be reflected; nobody minds)
SCOPE v1
IN: like, unlike, count_likes, uniqueness by (user, post)
OUT: list of who liked, notifications, ranking of
posts by likes, anti-fraud
Notice the parallels and the differences with Enlace. Like Enlace, it is read-heavy (more count queries than new likes) and it tolerates eventual consistency (that a like takes seconds to appear in the count bothers nobody —the same kind of tolerance that makes resolve cacheable—). The key difference is in one functional requirement: F4, uniqueness (a user does not like twice), which Enlace does not have and which here conditions the data model (you need to know who liked what, not just the total). And in the scope, leaving out "the list of who liked" is the decision analogous to leaving out analytics in Enlace: it keeps the heart simple (a counter) and defers the part that would multiply the storage and the reads. The method transfers: different domain, same step 1 structure.
Summary and next step
In this lesson you executed step 1 of the framework for Enlace, the foundation of the capstone: the three artifacts that form the contract of the system. The functional ones (F1 shorten, F2 resolve with 302, F3 the 404) that fit in three lines —because Enlace's difficulty is not in what it does—. The non-functional ones with a number (~40 writes/s, ~4,000 reads/s, ~6 TB, latency <100 ms, 99.9% availability, eventual consistency) —each one a future justification of a box of the design—. And the scope table, where the most powerful decision was to defer analytics, because incrementing clicks on every read would multiply the writes by ~100 and change the entire system.
You saw, with the construction contract, why requirements go before the blueprints: they are the yardstick against which each component of the design is measured, and the correct order is contract → blueprints → construction. And you produced the compact step 1 block that opens the final deliverable.
Before moving on you should be able to: write Enlace's three functional requirements as a contract (with the 404); list the non-functional ones with their number; build the scope table in both directions; and explain why deferring analytics is a design decision, not laziness.
What comes next is turning the numbers of step 1 into the capacity table. In lesson 3 you execute step 2 end to end: you run the napkin math in Python —QPS, storage at 5 years, bandwidth, working set memory— and reproduce each anchor number instead of quoting it. Today's non-functional requirements are the inputs of those calculations; their results will be the signals that draw the design of lesson 4.
Resources
- System Design Primer — "Step 1: Outline use cases, constraints, and assumptions" — the Primer step that corresponds exactly to this lesson: clarifying use cases (functional), constraints (non-functional) and assumptions before drawing. It insists, like us, on declaring the scope explicitly.
- Designing Data-Intensive Applications, Kleppmann — Chapter 1, "Thinking About Data Systems" — the definition of the three non-functional pillars (reliability, scalability, maintainability) that structure this lesson's non-functional table. The canonical vocabulary for writing requirements that can be designed.
- Google SRE Book — "Service Level Objectives" — how a fuzzy non-functional requirement ("make it reliable") is turned into an objective with a number (99.9% availability, p99 latency < 100 ms). The foundation of why this lesson insists that every non-functional requirement carries its figure.