Module 1: How to Approach a System Design Problem
2. Functional vs. non-functional requirements
Description
In the previous lesson you installed the reflex of understanding before you draw. Now we sharpen the tool you understand with: the distinction between functional and non-functional requirements. It's the most important distinction in the whole module, and the one most people confuse. A functional requirement says what the system does: "it shortens a URL", "it redirects the visitor", "it returns 404 if the code doesn't exist". A non-functional requirement says how well it does it: "it handles 4000 reads per second", "it redirects in under 100 milliseconds", "it's available 99.9% of the time". The first are verbs —capabilities—; the second almost always carry a number behind them —measurable qualities—.
Why does separating them matter so much? Because a system's design is almost never determined by the functional requirements; it's determined by the non-functional ones. Shortening and resolving URLs is trivial —an in-memory dictionary does it in ten lines—. What turns Enlace into a real engineering problem isn't what it does, but at what scale, with what latency, with what availability. Two systems with the same functional requirements —both "shorten URLs"— but different non-functional ones —one at a thousand a month, another at a hundred million— are two radically different architectures. That's why, when a prompt arrives vague, your number-one priority is to pull the non-functionals out of it: they're the ones that decide the design.
Connection to the module: this lesson is the foundation of the method. Lesson 1 told you "understand before you draw"; this one tells you what it is you understand —the two kinds of requirement—. Lesson 3 rests directly on top: the clarifying questions exist to fill the non-functional holes in the prompt (how many users?, how fast?, how available?). Lesson 5 will put "requirements" as the first step of the 4-step framework, and "requirements" means exactly this: the list of functional and non-functional ones. Without this clear distinction, everything else gets muddy.
The waiter who writes down two columns
Think of it this way. You walk into a restaurant and tell the waiter: "I want a burger." A good waiter doesn't run to the kitchen with just that. They write down two separate columns in their notepad. In the first column —which dish— they put: burger, medium, with cheese, no onion. That column defines what they'll bring you; if they get it wrong, you get the wrong dish. In the second column —how you want it served— they put things that don't change what the dish is, but do change how well it arrives: that it come out in under fifteen minutes because you're in a hurry, that it come hot, that the kitchen guarantee it touched nothing with peanuts because you're allergic, that if the beef runs out they tell you instead of leaving you waiting.
Notice the difference between the two columns. The first —the ingredients, the doneness— is what makes it your burger and not someone else's. The second —fast, hot, allergen-free, alert-me-if-something-fails— doesn't change the dish, but it determines whether the experience was good or a disaster. And here's the crucial part: the second column is what decides how the kitchen has to be set up. Serving one hot burger in fifteen minutes any kitchen can do. Serving five hundred hot burgers in fifteen minutes each, at rush hour, without anything happening to anyone with an allergy, demands an industrial kitchen with stations, protocols, and redundancy. The what (burger) didn't change; the how well (five hundred, hot, fast, safe) rebuilt the entire kitchen.
A software system is identical. The functional requirements are the first column: what dishes the system serves —shorten, resolve, redirect—. The non-functional requirements are the second: how fast, how many at once, how available, how secure. And just as in the restaurant, it's the second column that decides the architecture. A novice designer obsesses over the first —"what features does it have?"— and designs a one-burger kitchen. A seasoned designer asks about the second —"how many, how fast, with what guarantee?"— because they know that's where the real problem is.
It's worth spelling it out in full:
Functional requirements say what the system does; non-functional ones, how well it does it —and they almost always carry a number—. The design is mostly determined by the non-functional ones: that's why they're your priority when understanding a problem.
Enlace's functional requirements
Let's start with the easy column. Enlace's functional requirements are the capabilities of its core, and you already know them from lesson 1. Written as a list of verifiable behaviors:
FUNCTIONAL (what Enlace does)
F1. shorten(long_url) -> short_code
Takes a valid long URL and returns a unique short code.
F2. resolve(short_code) -> redirect to long_url
On visiting enla.ce/<short_code>, responds 301/302 with Location.
F3. If the short_code doesn't exist, responds 404.
F4. (optional) A link can expire on a date (expires_at).
F5. (optional) Count the clicks of each link (analytics).
Notice three things. First, each requirement is an observable behavior: you can say "yes, it meets it" or "no, it doesn't" looking at it from the outside, without knowing the internal design. Second, they're numbered and ordered by importance: F1, F2, and F3 are the heart (without them there's no shortener); F4 and F5 carry the (optional) label because they're features we'll decide whether to include when we scope in lesson 4 —not every functional requirement makes it into the first design—. Third, and this is the important part: this list is very short. Five lines, and two are optional. That brevity is the signal that Enlace's challenge doesn't live here.
Enlace's non-functional requirements
Here is where the challenge lives, and where almost all the guide's work will concentrate. A system's non-functional requirements usually fall into a handful of families. These are the ones that matter for Enlace, each with its number:
- Scale / throughput (how much it handles). How many operations per second? For Enlace: ~40 writes/s and ~4000 reads/s (we computed them in lesson 1). This pair of numbers is what screams "read-heavy system" and will decide half the design.
- Latency (how fast it responds). How long does an operation take? A shortener that takes two seconds to redirect is useless —the whole point is that the redirect be imperceptible—. A reasonable target: the redirect responds in under ~100 ms for the user. Watch out not to confuse latency (fast for one request) with throughput (many requests at once): they're different axes.
- Availability (how often it's on). What fraction of the time does the system respond? It's measured in "nines". For Enlace, a typical target is 99.9% ("three nines"). In a moment we'll compute how much downtime that allows.
- Durability (data isn't lost). Once Enlace has given you a short code, that link can't disappear —if tomorrow it doesn't resolve, you've broken all the links people already shared—. It's a very hard, silent non-functional requirement.
- Consistency (everyone sees the same thing). When
shortenhas just created a code, does it have to resolve immediately everywhere, or is it fine for it to take a second to propagate? For Enlace, it turns out a little delay is fine (eventual consistency), and that's a decision with enormous consequences we'll develop in module 7.
Written as a table —which is how it's best to deliver them— Enlace's non-functionals look like this:
| Non-functional requirement | Target (the number) | Why it matters |
|---|---|---|
| Read throughput | ~4000 reads/s | Decides caching (M4) and balancing (M6) |
| Write throughput | ~40 writes/s | Comfortable for a single DB (M3, M5) |
| Redirect latency | < ~100 ms | The redirect must be imperceptible |
| Availability | 99.9% (three nines) | A downed shortener breaks all live links |
| Durability | don't lose links | A lost link is a broken link forever |
| Consistency | eventual is fine | A new link can take ~1 s to propagate |
Compare the two columns. The functional one has five lines of simple verbs. The non-functional one has six lines loaded with numbers and consequences, and each points to a future module. There it is, in plain sight, the thesis of the lesson: the design is run by the non-functionals.
Worked example: from a business sentence to an engineering number
The non-functionals are rarely handed to you as clean numbers. They're handed to you as business sentences, and your job is to translate them into engineering numbers. Let's see it with the two sentences Enlace starts from: "we expect 100 million new URLs a month" and "people visit them about 100 times more than they create them". Neither of those two sentences looks like a non-functional requirement —they sound like marketing—, but they are, in disguise. Let's translate them:
# from business sentences to measurable non-functional requirements
writes_per_month = 100_000_000 # "100M new URLs a month"
read_write_ratio = 100 # "visited 100x more than created"
seconds_per_month = 30 * 24 * 3600
qps_write = writes_per_month / seconds_per_month
qps_read = qps_write * read_write_ratio
# availability: translate "99.9%" to allowed downtime
availability = 0.999
seconds_per_year = 365 * 24 * 3600
downtime_per_year_h = seconds_per_year * (1 - availability) / 3600
print(f"write throughput = {qps_write:.0f} ops/s")
print(f"read throughput = {qps_read:.0f} ops/s")
print(f"at 99.9%, allowed downtime = {downtime_per_year_h:.2f} hours/year")
What to expect. Running this with Python 3.14.0:
write throughput = 39 ops/s
read throughput = 3858 ops/s
at 99.9%, allowed downtime = 8.76 hours/year
Look at what just happened. Two vague business sentences —"100 million a month", "a hundred times more reads"— turned into two hard engineering requirements: ~40 writes/s and ~4000 reads/s. And "99.9% availability", which sounds like a commercial promise, turned into a concrete budget: 8.76 hours of downtime per year, about 44 minutes a month. That translation —from business sentence to engineering number— is the central skill of this lesson. Without it, "we want high availability" is smoke; with it, it's "we have a budget of 8.76 hours down per year, so we need redundancy because a single machine doesn't guarantee it" —which links directly to module 7—.
It's worth seeing the full "nines" scale, because every extra nine costs an order of magnitude more effort:
| Availability | Downtime per year | Downtime per month | What it implies |
|---|---|---|---|
| 99% ("two nines") | 87.6 h | ~7.3 h | A whole afternoon down; cheap |
| 99.9% ("three nines") | 8.76 h | ~44 min | Enlace's typical target |
| 99.99% ("four nines") | 52.6 min | ~4.4 min | Demands real redundancy and failover |
| 99.999% ("five nines") | 5.3 min | ~26 s | Very expensive; only for critical systems |
Notice the jump: going from 99% to 99.9% cuts the downtime from 87 hours to 9. Going from 99.9% to 99.99%, from 9 hours to 53 minutes. Each additional nine is ten times less downtime —and much more expensive to achieve—. Choosing how many nines Enlace needs is not a whim: it's a tradeoff between cost and engineering, and that's why it's a non-functional requirement you decide with judgment, not "the maximum" by reflex.
Why the non-functionals run the design
We already said it, but it's worth seeing in action, because it's the idea that justifies the rest of the guide. Take the same Enlace —the same functional requirements, shorten and resolve— and change only one non-functional: the scale. Watch how the design transforms:
graph TD
subgraph Small["Enlace intranet: 1000 URLs/month"]
A1[One server] --> A2[(In-memory dictionary)]
end
subgraph Big["Enlace public: 100M URLs/month"]
B1[Load balancer] --> B2[Many servers]
B2 --> B3[(Cache)]
B2 --> B4[(DB with replicas and shards)]
end
Both "shorten URLs" —identical function—. But the one on the left, at a thousand a month, fits in an in-memory dictionary on one server; the one on the right, at a hundred million a month with 4000 reads/s, needs a load balancer, replicas, a cache, and sharding. Not a single functional requirement changed between the two. One non-functional number changed, and it rebuilt the entire architecture. That's the mechanical reason why, faced with a vague prompt, your first hunt is for non-functional numbers: they're the ones that tell you which of those two diagrams —or which point in between— you have to design.
And that's also why the rest of this guide is organized by non-functionals, not by functionals. Module 4 (caching) exists because of the read throughput requirement. Module 5 (replicas/sharding) exists because of the storage and throughput one. Module 6 (balancing) exists because of the compute throughput one. Module 7 (reliability and consistency) exists because of the availability, durability, and consistency ones. Each module of the guide is the answer to a non-functional requirement of Enlace. The functionals —shorten and resolve— were resolved in ten minutes; the non-functionals are the work of seven modules.
Common mistakes
Listing only functional requirements and calling it done. What happens: someone enumerates "shortens, resolves, redirects, counts clicks" and feels they've understood the problem. But they didn't say how many URLs, or how fast, or how available, so they have no idea which design to build —the in-memory dictionary or the distributed system?—. Why it happens: functionals are easy and visible; non-functionals have to be pulled out of the prompt with questions. How to detect it: if your requirements list doesn't have a single number, you're missing all the non-functionals. How to fix it: for each functional, ask yourself "at what scale, with what latency, with what availability?". Lesson 3 gives you the catalog of those questions.
Asking for "the maximum" on every non-functional. What happens: someone, by reflex, asks for "the maximum availability, the minimum latency, the greatest scale", as if more were always better. They end up justifying five nines (5 minutes of downtime per year) for an internal shortener nobody misses if it goes down for half an hour, and over-build by ten times the necessary cost. Why it happens: they forget that each non-functional is a tradeoff with cost, not a dial you crank to the top for free. How to detect it: if you can't justify why 99.9% and not 99.99%, you didn't choose, you copied. How to fix it: each non-functional is set at the level the business needs, not the maximum. Three nines for a public Enlace is a defensible decision; five nines would be throwing money away.
Confusing latency with throughput. What happens: someone says "the system is fast, it does 4000 operations per second" —mixing two different things—. Throughput (4000/s) is how many fit at once; latency is how long one takes. A system can have very high throughput and terrible latency (it processes millions per second but each one takes 10 s if it goes in a batch), or the other way around. Why it happens: both sound like "fast". How to detect it: if you use "fast" without saying whether you mean many at once or each one quick, you're confusing them. How to fix it: always separate them in your non-functional table —throughput in ops/s, latency in ms—. They're two requirements, with two numbers, optimized differently (caching lowers latency; balancing raises throughput).
Exercises
Exercise 1 — Classify and sort. Take this scrambled list of statements about Enlace and organize it into two columns, functional and non-functional: (a) "Shortens a long URL to a 7-character code." (b) "Supports 4000 redirects per second." (c) "Redirects the visitor to the original URL." (d) "Never loses an already-created link." (e) "Returns 404 if the code doesn't exist." (f) "Responds in under 100 ms." For each non-functional, also name which family it is (throughput, latency, availability, durability, consistency).
See solution
Functional (what it does):
- (a) Shortens a URL → it's the
shortenoperation. - (c) Redirects the visitor → it's the
resolveoperation. - (e) Returns 404 if it doesn't exist → behavior of the
resolvecontract.
Non-functional (how well):
- (b) 4000 redirects/s → throughput family (read).
- (d) Never loses a link → durability family.
- (f) Responds in < 100 ms → latency family.
Notice the mechanical hint: the functionals (a, c, e) are capability verbs ("shortens", "redirects", "returns"). The non-functionals (b, d, f) are measurable qualities with a number or a guarantee ("4000/s", "never loses", "< 100 ms"). When in doubt, ask yourself: "does this describe a new capability, or how well a capability I already have works?". The latter is always non-functional.
Exercise 2 — Translate a non-functional into a number. Enlace's owner says: "I want the service to be available practically all the time; I don't tolerate more than one hour of downtime a month." Translate that business sentence into an availability requirement in "nines" and decide which of the levels in the table meets it. (Hint: there are ~730 hours in a month; compute what percentage 1 hour of downtime represents.)
See solution
A month has approximately 30 × 24 = 720 hours (let's use ~730 for an average month). One hour of downtime out of 730 is:
- Fraction down = 1 / 730 ≈ 0.00137 → 0.137% of the time down.
- Availability = 1 − 0.00137 = 0.9986 → 99.86%.
That falls between "three nines" (99.9%, which allows ~44 min/month) and "two nines" (99%, which allows ~7.3 h/month). Since the owner doesn't tolerate more than one hour, and 99.9% allows only 44 minutes (stricter than one hour), 99.9% meets their requirement with room to spare; 99% (which would allow 7.3 hours) would not meet it. Defensible conclusion: we set the target at 99.9% ("three nines"), which respects the one-hour-a-month limit with margin. Notice the method: the business sentence ("one hour a month") became a number (99.86%), and the number was placed on the nines scale to choose the target. That's translating a non-functional.
Exercise 3 — The same system, another scale. Imagine Enlace, instead of being a public shortener, were the internal tool of a 500-employee company for sharing short links among themselves. Estimate (roughly, with back-of-the-envelope arithmetic) its new non-functional numbers: how many writes/s and reads/s would it have, instead of 40 and 4000? And answer: would the design change compared to the public Enlace? Why?
See solution
Let's estimate back-of-the-envelope style. 500 employees; suppose each one creates, generously, 10 links a day and visits 100 a day:
- Writes/day = 500 × 10 = 5,000. Over 86,400 seconds ≈ 0.06 writes/s (one every ~17 s).
- Reads/day = 500 × 100 = 50,000. Over 86,400 ≈ 0.6 reads/s.
Compare: the public Enlace does ~40 writes/s and ~4000 reads/s; the internal one does ~0.06 and ~0.6 —about a thousand times less—. Does the design change? Radically. At 0.6 reads/s, you don't need a cache, or replicas, or sharding, or a load balancer: a single server with an in-memory dictionary or a tiny database resolves everything, with enormous margin. The functional requirements are identical (shortens, resolves, redirects), but the non-functional ones collapse the seven-module design into a single box. This is the pure demonstration of the lesson's thesis: the functionals changed nothing, one non-functional (the scale) changed everything. And also a warning: building the public Enlace (with cache, replicas, and sharding) for this internal case would be textbook over-engineering.
Summary and next step
In this lesson you sharpened the central distinction of the method: functional (what it does) vs. non-functional (how well, with a number), like the waiter who writes down two columns. Enlace's functionals are five lines of simple verbs —shorten, resolve, redirect, 404, and two optional ones—. The non-functionals are six families loaded with consequences —throughput, latency, availability, durability, consistency— and each points to a future module of the guide.
The idea you take away, and which justifies the whole guide, is that the design is run by the non-functionals: the same Enlace at a thousand URLs/month fits in a dictionary, and at a hundred million/month needs a load balancer, cache, replicas, and sharding —without a single functional requirement changing—. That's why, faced with a vague prompt, your first hunt is for non-functional numbers. And you practiced the skill of translating: from "100M a month" to ~40 writes/s, from "a hundred times more reads" to ~4000 reads/s, from "99.9%" to 8.76 hours of downtime per year.
Before moving on you should be able to: classify any statement as functional or non-functional at a glance; name Enlace's five non-functional families with their number; translate a business sentence ("one hour of downtime a month") into an engineering number (99.86% → 99.9%); and explain why the design is run by the non-functionals.
What comes next is learning to obtain those numbers when the prompt doesn't give them to you —which is almost always—. In lesson 3, the clarifying questions: what to ask, in what order, and how to state a defensible assumption when there's no one to ask. The questions exist, precisely, to fill the non-functional holes you just learned to recognize.
Resources
- Designing Data-Intensive Applications (DDIA), Chapter 1 — official site — Kleppmann organizes the entire book around three non-functionals: reliability, scalability, and maintainability. Chapter 1 defines them one by one and is the best source for understanding why these requirements, and not the functional ones, dominate the design of data systems.
- System Design Primer — "Availability numbers" — the "nines" table (99.9%, 99.99%…) with the downtime each one allows, and how they combine when you put components in series or in parallel. It's the canonical reference for the availability calculation we did here.
- Google SRE Book — "Service Level Objectives" — Google's chapter on SLA, SLO, and SLI: how a non-functional target (like 99.9% availability) is formally set and how it's measured. We'll return to it in module 7; read it now to see where the "nines" come from.
- Latency Numbers Every Programmer Should Know — the classic list of reference latencies (reading from memory, from SSD, over the network, between continents). It gives you the intuition for what "fast" really is, indispensable for putting numbers on the latency non-functional requirement (< 100 ms) with your feet on the ground.