Learn › Engineering Process
·FREE·12 min read

The Craft of Requirements — Why "We Built Exactly What They Asked For" Fails

Why shipping precisely what a stakeholder asked for produces software that works and nobody uses. Stated versus latent needs, use cases, turning "fast" and "secure" into numbers, and a set of question templates that expose ambiguity on the spot.

Key points

  • What a stakeholder asks for is a tool, not the outcome; requirements engineering digs down to the outcome and rewrites it in a verifiable form.
  • Needs come in three layers: stated, latent, and assumed; the layer that causes accidents is the assumed one that nobody said.
  • The test of requirement quality is whether everyone who reads it can write the same test.
  • With A = 0.999 and T of 30 days, or 43,200 minutes, the permitted downtime D works out to 43.2 minutes.
  • A 200-millisecond mean is no comfort if one user in a hundred waits five seconds, so response time is written as percentiles.
ModalitytextTasksystems

A Quarter-Inch Drill and a Quarter-Inch Hole

There is an old line in marketing: people don't want a drill, they want a hole. What someone asks for is a tool. What they need is an outcome.

The same thing happens in software. Someone asks for a CSV export button. You build the button. It ships, it passes acceptance, everyone signs off. Six months later almost nobody uses it. Ask again and it turns out what they actually wanted was "get the month-end numbers to accounting," and CSV was simply the only mechanism they could think of at the time. The thing that would have mattered was pushing the numbers straight into the accounting system.

"We built exactly what they asked for" is the easiest excuse to say and the least useful one to have. Requirements engineering is the work of neither swallowing nor dismissing what a stakeholder says, but digging down to the outcome underneath it and rewriting it in a form you can verify. Sometimes the rewrite comes out identical to the original request. Even then, arriving there by checking is a completely different situation from arriving there by assuming.

Needs Come in Three Layers

What is in a stakeholder's head does not arrive in the order they say it. It sits roughly in three layers.

Stated needs are the ones they can put into words themselves. "I want a CSV export." "I need a login screen." These come out when you ask — but notice that by the time they surface, they have already been shaped into somebody's proposed solution.

Latent needs are the ones they would be delighted to have but will never volunteer, for the simple reason that they don't know such a thing can be built. "I spend twenty minutes a month retyping these figures by hand" will come out if you ask the right way. "Please automate that retyping" will not. People can only assemble requests out of options they already know exist.

Assumed needs are the ones considered too obvious to be worth saying. "The totals don't drift." "Nobody can see someone else's salary." "Pressing save actually saves." Meeting these earns you no credit; violating one destroys trust instantly. The Kano model in quality engineering names this asymmetry directly, separating must-be quality (expected by default, a source of dissatisfaction when absent) from attractive quality (not missed when absent, delightful when present). The layer that causes accidents is the first one: nobody said it, so nobody built it, and after release you hear "well, obviously."

So what happens if you carefully collect only the stated needs and build those? You get a system that behaves perfectly on the cases you heard about and falls apart on the ones you didn't. The shape of that failure is very close to overfitting in machine learning.

FIG 1As an analogy. The harder you fit the examples you gathered in interviews (the training data), the lower the error there — while the error on the cases you never heard about (the test data) starts climbing. The degree slider stands in for how tightly you fit "exactly what they asked for"

"Need" and "Requirement" Are Different Words

These get used interchangeably in conversation, but they belong to different stages of work. ISO/IEC/IEEE 29148, the international standard for requirements engineering, treats stakeholder requirements and system requirements as distinct artifacts.

"I want to stop retyping figures into accounting" is a need. "The system shall transmit the current month's journal entries to the accounting system's API by 09:00 on the first business day after close" is a requirement. Two different people can judge the second one identically. That is the whole test of requirement quality: can everyone who reads it write the same test?

Use Cases: Putting a Person Back in the Subject Position

Write features as a bullet list and the subject quietly becomes "the system" everywhere, which drops out who is doing this, why, and in what order. A use case rewrites the same thing from the human side. The minimum viable form has five parts.

Most of the value is in the last two. The main scenario comes out roughly the same no matter who writes it. But the moment you try to write alternate flows, you are forced to ask "and what happens when that fails?" — and any place where the answer is undefined becomes visible on the spot. Postconditions are the act of deciding, up front, what you will look at to declare the thing done.

Non-Functional Requirements: Turning "Fast" and "Secure" Into Numbers

If functional requirements say what the system can do, non-functional requirements say how well it does it. ISO/IEC 25010 organizes these as quality characteristics — performance efficiency, reliability, security, maintainability, usability, and others. The awkward part is that non-functional requirements constrain your design in ways you cannot back out of. Features can be bolted on later; "under one second" and "updates with zero downtime" are extraordinarily expensive to add after the architecture is fixed.

Availability. "Make it so it doesn't go down" is not a requirement. Pick an availability target and the permitted downtime falls out of it automatically.

D=(1−A) TD = (1 - A)\,T
(1)

DD is the downtime you are allowed, AA is the availability target as a fraction between 0 and 1, and TT is the length of the period — in words, it multiplies the fraction you failed to achieve by the length of the period. With A=0.999A = 0.999 (three nines) and TT of 30 days, or 43,200 minutes, DD works out to 43.2 minutes. That is an agreement that the system may be down for 43 minutes a month. Only once it is that concrete do the questions worth arguing about appear: does planned maintenance count against those 43 minutes, and is an outage at 3 a.m. treated the same as one at noon?

Response time. The trap here is the average. A 200-millisecond mean is no comfort if one user in a hundred waits five seconds — for that person the product is broken. So you write percentiles instead. "p99 of 300 ms" means everything except the slowest 1% comes back within 300 ms.

This starts to bite the moment one screen calls several services behind it. Let pp be the probability that a single call comes back inside its budget and nn the number of calls per request. The probability that all of them make it is:

P=p nP = p^{\,n}
(2)

PP is the probability that one whole request finishes inside its budget; in words, the formula just multiplies the "fast" probability by itself once per call. It assumes the calls are slow independently of each other, which is optimistic — in reality a congested shared database makes them slow together. Even so, plug in p=0.99p = 0.99 and n=100n = 100 and you get P≈0.37P \approx 0.37. Every individual service honors its "99% are fast" promise, and two requests out of three are still slow at the screen level. The intuition that per-component percentiles compose into a system-level guarantee does not survive that multiplication. Non-functional requirements have to be set on the unit the user actually experiences, before they are divided up among components.

Whether you are actually meeting those numbers in production is the subject of Observability — Logs, Metrics, and Traces in Practice.

Question Templates That Expose Ambiguity

The most practical skill in this whole discipline is being able to fire back a fixed-shape question the instant you see a vague sentence. It isn't talent, it's a set of patterns. Dangerous phrasings come in recognizable types, and each one hides a specific thing.

Dangerous phrase What it hides The question to fire back
all / always / every time The exceptions Are there truly zero exceptions? If even one exists, what happens then?
fast / lightweight / easy to use The baseline and the threshold Faster than what? Which operation, on which connection, within how many seconds counts as a pass?
"is notified" (passive voice) The missing subject Who performs it? Who notices when it fails?
user / data / appropriately Undefined terms Does that include logged-out visitors? Deactivated accounts? Admins?
and so on / etc. A truncated list Name three things inside the "etc." Is that all of them?
integrate with / automatically Undefined behavior Within how many seconds? On failure, does it retry or give up?
I want to be able to... Means confused with ends If you could do that, what specifically gets better?

Three more patterns are worth memorizing. The first is asking in the negative. "What would count as failure?" pulls out far more concrete answers than "what would success look like?" People describe desirable states vaguely and describe accidents they want to avoid in vivid detail.

The second is making them give something up: "If speed and accuracy can't both win, which one do we sacrifice?" Requirements agreements usually collapse because two mutually incompatible things were both agreed to, and this question surfaces that before design starts. If the answer is "we need both," you have learned that the real decision-maker is someone else in the room.

The third is making them write down one concrete instance. An abstract requirement admits infinite readings; a real example admits one. Collect three real examples and by the second or third you will hear "oh, this one's handled as an exception, though." That exception is a requirement you would never have obtained by asking.

Cases Multiply, They Don't Add

As requirements accumulate, the implementation and test surface does not grow in proportion to the number of requirements. It multiplies. With kk kinds of conditions and DiD_i the set of values the ii-th condition can take, the total number of combinations is:

N=∏i=1k∣Di∣N = \prod_{i=1}^{k} |D_i|
(3)

∣Di∣|D_i| is how many values the ii-th condition can take, and ∏\prod means multiply all of them together — in words, adding one condition is not adding one line — it is taking every case you already have and duplicating the whole set. Four membership tiers × five payment methods × three countries × three device types gives N=180N = 180. Add a single "first purchase or not" boolean and it becomes 360. For nn booleans, it is 2n2^n.

FIG 2The number of cases produced by n boolean conditions is exactly the O(2ⁿ) curve. Toggling between linear and log scales shows, in orders of magnitude, how heavy the decision to add one more condition really is

This is why the highest-leverage move in requirements work is deleting conditions rather than adding them: "does this branch actually occur in the business, or is it just in case?" Just-in-case branches survive untested and become, a few years later, behavior nobody can explain. When exhaustive combinations are out of reach there are pragmatic reductions like pairwise testing, which covers every pair of conditions rather than every combination — but shrinking kk in the first place is always cheaper.

Making Acceptance Criteria Executable

Once a requirement is a number, the judgment can be code. "Make it faster" reduced to an executable predicate looks like this:

def meets_slo(latencies_ms, p=0.99, budget_ms=300):
    """Does this sample of response times meet the p-quantile target?"""
    xs = sorted(latencies_ms)
    idx = int(p * (len(xs) - 1))     # position of the p-quantile
    return xs[idx] <= budget_ms      # is p99 within 300 ms?

It is under ten lines, but whether you can write it at all is the test of requirement quality. If you can't, you don't have a requirement yet. The prose equivalent is Given-When-Then acceptance criteria: "Given the current month's sales are finalized, when the close job runs, then journal entries for the current month exist in the accounting system." Precondition, action, observable result — the skeleton of a test, already written.

Deciding What Not to Decide

Half of what belongs in a requirements document is the things you are not doing. Non-goals are what you explicitly declare out of scope; leave them out and their absence gets read as "obviously included." Assumptions are things you are proceeding as if true without having verified them — "every counterparty supports the API." Writing them down means that when one collapses, you know immediately what to revisit. Open questions get recorded together with who decides them and by when. Left blank, an implementer fills them in with good intentions and that judgment becomes the spec without anyone else ever seeing it.

How This Is Used on the Job

Who, and when. Product managers when framing a new feature, tech leads before entering design, SREs when setting production performance targets, QA at the front of test design. In contract work the critical moment is before the estimate — an estimate built on vague requirements is almost guaranteed to be wrong.

What you actually touch. Use case descriptions and Given-When-Then acceptance criteria; SLOs and error budgets; a non-functional checklist laid out along the ISO/IEC 25010 quality characteristics; ADRs (Architecture Decision Records) that keep one decision per file; a traceability matrix mapping requirements to tests. Tooling tends to mean issue templates in Jira or Linear, spec pages in Confluence or Notion, and frameworks like Cucumber or pytest-bdd that execute acceptance criteria directly.

Pitfalls that turn into incidents.

  1. Deferring the non-functional. "We'll measure performance later," then "p99 300 ms" lands after the architecture is frozen, and you rebuild. Put numbers on non-functionals in the first sprint.
  2. Setting SLOs on the mean. Averages hide your unhappiest users. Always percentiles.
  3. Agreements that exist only in meeting notes. He-said-she-said is guaranteed. Version-control requirements as an artifact so changes are reviewable as diffs.
  4. Estimating vague words. The instant you convert "integrate with" into person-days, that estimate stops being a commitment to anything. Items still carrying vague words should be cut as investigation tasks, not estimates.
  5. Freezing requirements and treating change as failure. Requirements changing because you learned something is normal. What to forbid is not change itself, but change slipped in without evaluating its blast radius.

How it shows up in design reviews. "Name three ambiguities in this requirement" is a standard prompt, and the answer follows the table above: adjectives with no baseline, passive voice with no subject, undefined nouns. The other standard prompt is "name three non-functional requirements and how you'd measure each." Availability needs a definition of downtime and a measurement point; response time needs a percentile and a vantage point (client-side or server-side); security needs a statement of what is being protected and a threat model behind it.

Systems that include machine learning add "how do we define accuracy?" to this list, which makes the whole exercise harder; that design space is covered in ML System Design — The 90% Outside the Model, and deciding whether a change actually helped is covered in Hypothesis Testing and A/B Tests.

Summary

Published by TechAthletes · Editorial policy · How we cite · Corrections

Comments

Sign in to comment