---
title: 'Functional and Non-Functional Requirements'
source: 'https://academia.sh/en/courses/introduction-to-system-design/functional-and-non-functional-requirements'
course: 'Introduction to System Design'
language: en
updated: '2026-08-23T07:01:25+00:00'
license: 'CC BY-SA 4.0'
---

# Functional and Non-Functional Requirements

Showing that the two kinds of requirements are separate determinants: the functional list determining which kind of component is required, thresholds determining how many parts the same work splits into, the change a single threshold produces in the component count when played one at a time, and a threshold's ability to shrink a design.

The previous lesson separated questions into four families and marked that two families ask
different things: actor and flow questions ask what the system will do, threshold questions ask
within what limit it will do it. If two sets of answers can change independently of each other,
they are two separate determinants.

This lesson shows that independence. The functional requirement list states which kind of
component must exist; thresholds state how many parts the same work splits into. When the two are
played separately, different things change in the design, and that difference is countable.

## Two Determinants

A **functional requirement** states what the system does and is answered as true or false: the
state is returned given a tracking number, the same event arriving a second time is swallowed.
The Quality Attributes lesson of the Quality and Testing Fundamentals course had established the
same concept under the name "functional expectation"; here the catalog term, functional
requirement, is used, and the meaning intended is the same.

A **non-functional requirement** states within what limit the system does this work, and it is
answered by degree. The quality attribute and quality scenario concepts established in that same
lesson are not redefined here; a non-functional requirement is the written form of a quality
scenario that places a threshold on a quality attribute.

The test of the distinction is this: if a requirement can be checked with a single input–output
pair, it is functional. If checking it requires a load, an environment, and a threshold, it is
non-functional.

## A Threshold Has No Class, but It Has a Source

The first lesson separated numbers into three classes: assumption, computed value, measurement. A
threshold is none of these three, and this is deliberate. An assumption is a guess about the
world, a computed value comes out of assumptions, a measurement comes out of an apparatus; all
three report a fact. A threshold, by contrast, reports not a fact but an acceptance: which value
is counted as sufficient.

This is why what is written next to a threshold is not a class but a **source**: which
expectation the threshold comes from. The source of the threshold "the tracking response does not
exceed 200 milliseconds" is the response the recipient expects from the tracking page; the source
of "event loss is zero" is the delivery obligation in the carrier contract. A threshold with no
source written down cannot be argued about, because it is not known on whose behalf it was set.

A non-functional requirement is written in exactly this form — four parts from the quality
scenario, and the fifth line is the source field this course adds:

- **Stimulus:** the recipient asks about the shipment's status with a tracking number.
- **Environment:** the system is operating at peak load.
- **Response:** the shipment's final state, its zone, and its update time are returned.
- **Response measure:** the median response time does not exceed 200 milliseconds.
- **Threshold source:** the response the recipient expects from the tracking page.

The second part is empty for now. "At peak load" is not a number, and this scenario is currently
unmeasurable; turning the environment into a number is the next lesson's job.

## The Same Function, Two Threshold Sets

The way to show independence is to hold one side fixed and play the other. Six functional
requirements are held fixed, and two different threshold sets are tried.

The move from threshold to component is made with a set of rules. These rules are a **model**,
not a universal derivation: each rule ties a threshold condition to a component change, and its
rationale stands beside it. The fourth of the rules works in reverse of the other three and
removes a component.

```js
// design/requirement.mjs — functional list, threshold sets, and the model producing a component list from both
export const FUNCTIONAL = [
  { no: "F1", flow: "read", action: "return the shipment's state given trackingNo" },
  { no: "F2", flow: "read", action: "return the shipment's route history" },
  { no: "F3", flow: "write", action: "record the state event coming from the carrier" },
  { no: "F4", flow: "write", action: "swallow the same event arriving a second time" },
  { no: "F5", flow: "batch", action: "scan the seller's day and produce a fee" },
  { no: "F6", flow: "batch", action: "deliver the fee report as a file" },
];

export const THRESHOLD = {
  loose: { readMs: 900, eventLoss: 0.01, batchHours: 12, feeMs: 40 },
  tight: { readMs: 200, eventLoss: 0, batchHours: 4, feeMs: 1 },
};

export const SOURCE = {
  readMs: "the response the recipient expects from the tracking page",
  eventLoss: "the delivery obligation in the carrier contract",
  batchHours: "the seller wanting the report in the morning",
  feeMs: "the time the scan can spend per shipment",
};

// The functional list determines which kind of component must exist.
export function baseline(functional) {
  const b = new Set(["service", "shipment-store"]);
  if (functional.some((i) => i.action.includes("fee"))) b.add("tariff-service");
  return b;
}

// Thresholds determine how many parts the same work splits into.
export const RULE = [
  { name: "read threshold under 300 ms", condition: (e) => e.readMs <= 300,
    add: ["tracking-endpoint", "event-receiver"], remove: ["service"] },
  { name: "event loss is not tolerated", condition: (e) => e.eventLoss === 0,
    add: ["event-queue"], remove: [] },
  { name: "batch window under 6 hours", condition: (e) => e.batchHours < 6,
    add: ["batch-worker"], remove: [] },
  { name: "fee call under 1 ms", condition: (e) => e.feeMs <= 1,
    add: [], remove: ["tariff-service"] },
];

export function design(functional, threshold) {
  const component = baseline(functional);
  const triggered = [];
  for (const r of RULE) {
    if (r.condition(threshold) === false) continue;
    for (const b of r.remove) component.delete(b);
    for (const b of r.add) component.add(b);
    triggered.push(r.name);
  }
  return { component: [...component].sort(), triggered };
}
```

```js
// design/derive.mjs — the same functional list gives two different designs with two threshold sets
import { FUNCTIONAL, RULE, SOURCE, THRESHOLD, design } from "./requirement.mjs";

const count = (a) => FUNCTIONAL.filter((i) => i.flow === a).length;
console.log(`functional requirement = ${FUNCTIONAL.length} (read ${count("read")}, write ${count("write")}, batch ${count("batch")})`);
console.log(`${"threshold".padEnd(11)}${"loose".padStart(7)}${"tight".padStart(6)}  threshold source`);
for (const a of Object.keys(SOURCE)) {
  console.log(`${a.padEnd(11)}${String(THRESHOLD.loose[a]).padStart(7)}${String(THRESHOLD.tight[a]).padStart(6)}  ${SOURCE[a]}`);
}

for (const [name, threshold] of Object.entries(THRESHOLD)) {
  const t = design(FUNCTIONAL, threshold);
  console.log(`\n${name}: rules triggered ${t.triggered.length}/${RULE.length}, components ${t.component.length}`);
  console.log(`  ${t.component.join(", ")}`);
}
```

```
functional requirement = 6 (read 2, write 2, batch 2)
threshold    loose tight  threshold source
readMs         900   200  the response the recipient expects from the tracking page
eventLoss     0.01     0  the delivery obligation in the carrier contract
batchHours      12     4  the seller wanting the report in the morning
feeMs           40     1  the time the scan can spend per shipment

loose: rules triggered 0/4, components 3
  service, shipment-store, tariff-service

tight: rules triggered 4/4, components 5
  batch-worker, event-queue, event-receiver, shipment-store, tracking-endpoint
```

The six functional requirements are the same in both runs. With loose thresholds, no rule
triggers and the design stays at three components; with tight thresholds, four rules trigger and
the design rises to five components. Whether a design is "three components" or "five components"
therefore cannot be read from the functional list.

The composition of the five components is also worth noting: `service` and `tariff-service` are
not in the list, and `tracking-endpoint`, `event-receiver`, `event-queue`, and `batch-worker`
stand in their place. Because the fourth rule removes a component, tightening does not raise the
component count in a single direction. The event queue appearing as a component does not mean the
asynchronous delivery mechanism is explained here; the message queue was established in the
Caching, Queues and Asynchronous Processing course, and here it is only counted as a component.

## Two Directions of Independence

Independence must run in both directions: when an addition is made to the functional list, the
thresholds must stay in place; when a threshold is played, the functional list must stay in place.
The second script tests both.

```js
// design/independence.mjs — playing the functional list and each threshold one at a time and measuring the change in component count
import { FUNCTIONAL, THRESHOLD, design } from "./requirement.mjs";

const base = design(FUNCTIONAL, THRESHOLD.loose);
console.log(`start: functional ${FUNCTIONAL.length}, threshold loose, components ${base.component.length}`);

const F7 = { no: "F7", flow: "read", action: "return the shipment's estimated delivery day" };
const wider = design([...FUNCTIONAL, F7], THRESHOLD.loose);
console.log(`functional added (${F7.no}): functional ${FUNCTIONAL.length + 1}, components ${wider.component.length}, ` +
  `change ${wider.component.length - base.component.length}`);

console.log(`\n${"single threshold change".padEnd(28)}${"components".padStart(11)}${"change".padStart(8)}  removed/added`);
for (const a of Object.keys(THRESHOLD.loose)) {
  const t = design(FUNCTIONAL, { ...THRESHOLD.loose, [a]: THRESHOLD.tight[a] });
  const removed = base.component.filter((b) => t.component.includes(b) === false);
  const added = t.component.filter((b) => base.component.includes(b) === false);
  const note = [removed.length ? `-${removed.join(" -")}` : "", added.length ? `+${added.join(" +")}` : ""].filter(Boolean).join(" ");
  console.log(`${`${a} ${THRESHOLD.loose[a]} -> ${THRESHOLD.tight[a]}`.padEnd(28)}${String(t.component.length).padStart(11)}` +
    `${String(t.component.length - base.component.length).padStart(8)}  ${note}`);
}
```

```
start: functional 6, threshold loose, components 3
functional added (F7): functional 7, components 3, change 0

single threshold change      components  change  removed/added
readMs 900 -> 200                     4       1  -service +event-receiver +tracking-endpoint
eventLoss 0.01 -> 0                   4       1  +event-queue
batchHours 12 -> 4                    4       1  +batch-worker
feeMs 40 -> 1                         2      -1  -tariff-service
```

The first two lines give one direction: when the seventh functional requirement is added, the
component count stays at 3, a change of 0. The new function falls inside the existing read flow
and does not move the component list. The intuition that a design grows as the functional list
grows is falsified by this line.

The table gives the other direction. When each of the four thresholds is played on its own, the
functional list stays at six items, yet the component count changes: three thresholds raise it
from 3 to 4, one lowers it from 3 to 2. The last row is the most instructive. When the fee call's
budget is pulled from 40 milliseconds to 1 millisecond, the tariff service cannot stay a separate
component, because being a separate component costs a network hop, and that hop does not fit the
budget. A tight threshold does not always ask for more parts; sometimes it asks for the opposite.

The removed-versus-added distinction in the first row also carries information. When the read
threshold tightens, `service` is removed and two components take its place, meaning this is not
an addition but a **split**. The other two thresholds only add. Whether a design change is a
split or an addition determines its effect on the contract surface: a split divides an existing
boundary in two, an addition brings in a new boundary.

## Summary

- A functional requirement states what the system does and is checked with a single
  input–output pair; a non-functional requirement states within what limit it does it and
  requires a load, an environment, and a threshold.
- A threshold has no assumption, computed value, or measurement class; a threshold is an
  acceptance, and what is written next to it is not a class but a source — which expectation it
  comes from.
- A non-functional requirement is written with the quality scenario's four parts plus the
  threshold's source; the example scenario's environment part is still numberless.
- The same six functional requirements gave 3 components with loose thresholds and 5 with tight
  thresholds; the component count cannot be read from the functional list.
- The seventh functional requirement did not change the component count (a change of 0); each of
  the four thresholds changed it on its own (three by +1, one by −1).
- A tight threshold does not always ask for more components: when the fee call's budget was
  pulled to 1 millisecond, the tariff service stopped being a separate component.

## Next Step

Three of the quality scenario's four parts were written, one stayed empty: the environment. The
sentence "operating at peak load" leaves the threshold unmeasurable, because how many requests
peak load is has not been written down. The same gap exists in the thresholds themselves —
whether 200 milliseconds is achievable cannot be said without knowing how many requests arrive
per second. The next lesson fills this gap: the six unanswered questions that turned into six
assumptions in the previous lesson are gathered into an assumption table, and request rate, data
growth, and bandwidth need are derived from the table by arithmetic.
