---
title: 'The Quality Attribute Tree'
source: 'https://academia.sh/en/courses/architecture-governance/quality-attribute-tree'
course: 'Quality Attributes and Governance'
language: en
updated: '2026-08-23T07:01:05+00:00'
license: 'CC BY-SA 4.0'
---

# The Quality Attribute Tree

Breaking quality attributes into sub-attributes and leaves: how many of thirty leaves tie to a number, how many of those turn into an executable check, how many of a known set of violations a check catches versus misses, and the person-hour cost of what stands in for the leaves that cannot be checked.

The previous course built edges, separated ownership, drew the line between what gets bought
and what gets built in-house. Decisions were written down with a rationale; laid side by side, the rationales show a shared
gap: none said which **attribute** it protected. What does the decision "the catalog is bought in" defend —
development time, maintainability, portability? If the answer is not written down, the decision
cannot be argued about later — the argument has no scale.

The definition of a quality attribute and the fields of a quality attribute scenario were built
in the Architectural Decisions and Documentation course, and testability measured there; not repeated here. The question here sits one step back: which attributes are on hand, which sub-attributes
they split into, whether every leaf ties to a number. The
course's own measure follows from this — whether an attribute turns into an
**executable check**, and if not, what stands in its place.

## Building the Tree

The block below **models** the quality attribute tree of the fictional regional library
network: five root attributes, each splitting into sub-attributes, and those into leaves. The
leaf is the only place in the tree that carries a measure; the root and the sub-attribute are
only catalog headings.

```js
// tree.mjs — models the quality attribute tree and counts how measurable each leaf is
import { mkdirSync, writeFileSync } from "node:fs";

// QA1: the regional library network is fiction (branches, a bought-in catalog, in-house loan
// and billing services, a separate membership system, the municipal identity service). The tree
// is a model: [code, root/sub, leaf, observation, unit, threshold, direction, source]; source
// "auto" = read from a log, "manual" = needs a measurement session, "-" = not tied to a number.
const DEFINITION = [
  ["B1", "performance/response", "catalog search latency", "search_ms", "ms", 400, "low", "auto"],
  ["B2", "performance/response", "loan transaction latency", "loan_ms", "ms", 600, "low", "auto"],
  ["B3", "performance/response", "report screen load", "report_s", "s", 8, "low", "auto"],
  ["B8", "performance/response", "what slowness feels like to the member", "", "", null, "", "-"],
  ["B4", "performance/volume", "hourly loan transactions", "loan_hour", "txn", 1200, "high", "auto"],
  ["B5", "performance/volume", "bulk catalog upload", "upload_min", "min", 90, "low", "auto"],
  ["B6", "performance/resource", "edge unit memory", "memory_mb", "MB", 512, "low", "auto"],
  ["B7", "performance/resource", "connection pool saturation", "pool", "%", 80, "low", "auto"],
  ["E1", "availability/outage", "monthly central outage", "outage_min", "min", 45, "low", "auto"],
  ["E2", "availability/outage", "loan continuing while offline", "offline_min", "min", 120, "high", "manual"],
  ["E3", "availability/recovery", "failover time", "failover_s", "s", 60, "low", "manual"],
  ["E4", "availability/recovery", "data loss window", "loss_min", "min", 5, "low", "auto"],
  ["E5", "availability/spread", "branches affected by an outage", "affected", "branch", 1, "low", "manual"],
  ["E6", "availability/spread", "information given to the member during an outage", "", "", null, "", "-"],
  ["G1", "security/identity", "unauthenticated endpoint", "open_endpoint", "endpoint", 0, "low", "auto"],
  ["G2", "security/identity", "staff action without a role", "no_role", "txn", 0, "low", "auto"],
  ["G3", "security/confidentiality", "plaintext secret value in storage", "open_secret", "value", 0, "low", "auto"],
  ["G4", "security/confidentiality", "history crossing outside its boundary", "out_of_bounds", "field", 0, "low", "auto"],
  ["G5", "security/traceability", "admin action without a record", "unlogged", "txn", 0, "low", "auto"],
  ["G6", "security/traceability", "whether the trust boundary is in the right place", "", "", null, "", "-"],
  ["S1", "maintainability/change", "files touched by a fee change", "file", "file", 6, "low", "auto"],
  ["S2", "maintainability/change", "manual steps to add a new branch", "step", "step", 3, "low", "manual"],
  ["S3", "maintainability/understandability", "module dependency depth", "depth", "level", 5, "low", "auto"],
  ["S4", "maintainability/understandability", "whether the code is readable", "", "", null, "", "-"],
  ["S5", "maintainability/testability", "loan rule coverage", "coverage", "%", 70, "high", "auto"],
  ["S6", "maintainability/testability", "test suite run time", "test_min", "min", 12, "low", "auto"],
  ["T1", "portability/installation", "edge unit install time", "install_min", "min", 45, "low", "manual"],
  ["T2", "portability/installation", "environment-specific call", "env_call", "call", 0, "low", "auto"],
  ["T3", "portability/data", "field lost in transfer", "lost", "field", 0, "low", "auto"],
  ["T4", "portability/data", "membership data handoff", "", "", null, "", "-"],
];

const FIELD = ["code", "branch", "name", "observation", "unit", "threshold", "direction", "source"];
const LEAF = DEFINITION.map((t) => Object.fromEntries(FIELD.map((a, i) => [a, t[i]])));

// Measurability has two steps: does the leaf tie to a number (does it have a unit and a
// threshold), does that number turn into an executable check (is its observation read from a log).
const numbered = (y) => y.threshold !== null;
const checkable = (y) => numbered(y) && y.source === "auto";

mkdirSync("tree", { recursive: true });
writeFileSync("tree/tree.mjs", `export const LEAF = ${JSON.stringify(LEAF)};\n`);

const root = (y) => y.branch.split("/")[0];
const count = (l, f) => String(l.filter(f).length).padStart(9);
console.log(`${"root attribute".padEnd(20)}${"sub".padStart(4)}${"leaf".padStart(8)}` +
  `${"check".padStart(9)}${"manual".padStart(9)}${"unnumbered".padStart(11)}`);
for (const k of [...new Set(LEAF.map(root))]) {
  const y = LEAF.filter((n) => root(n) === k);
  console.log(`${k.padEnd(20)}${String(new Set(y.map((n) => n.branch)).size).padStart(4)}` +
    `${String(y.length).padStart(8)}${count(y, checkable)}` +
    `${count(y, (n) => n.source === "manual")}${count(y, (n) => !numbered(n))}`);
}
const s = LEAF.filter(numbered), o = LEAF.filter(checkable);
console.log(`total ${LEAF.length} leaves: ${s.length} tie to a number, ` +
  `${LEAF.length - s.length} do not; ${o.length} turn into an executable check, ` +
  `${s.length - o.length} need a manual measurement`);
const list = (f) => LEAF.filter(f).map((y) => `${y.code} ${y.name}`).join("; ");
console.log(`\ncannot tie to a number: ${list((y) => !numbered(y))}`);
console.log(`has a threshold but no log: ${list((y) => numbered(y) && y.source === "manual")}`);
```

```
root attribute       sub    leaf    check   manual unnumbered
performance            3       8        7        0        1
availability           3       6        2        3        1
security               3       6        5        0        1
maintainability        3       6        4        1        1
portability            2       4        2        1        1
total 30 leaves: 25 tie to a number, 5 do not; 20 turn into an executable check, 5 need a manual measurement

cannot tie to a number: B8 what slowness feels like to the member; E6 information given to the member during an outage; G6 whether the trust boundary is in the right place; S4 whether the code is readable; T4 membership data handoff
has a threshold but no log: E2 loan continuing while offline; E3 failover time; E5 branches affected by an outage; S2 manual steps to add a new branch; T1 edge unit install time
```

Twenty-five of the thirty leaves tie to a number, five do not. The real distinction sits at the
second step: only 20 of those 25 turn into an executable check. The five in between have both a
threshold and a unit written down — what is missing is the **observation**. Whether failover
stays under 60 seconds cannot be learned without running a drill. Writing a threshold and tying a
measurement to a log are separate work; unpaid, the second leaves the attribute looking
measured but unchecked.

The distribution is not even root by root. Five of security's six leaves turn into a check,
since most are a count: how many endpoints lack authentication, how many plaintext
secret values sit in storage. Only two of availability's six leaves turn into a check; the rest
require breaking the system to observe. This does not say which attribute matters more; only which one is **cheaper to check**.

## Checks Against a Known Set of Violations

Twenty checks got written; the next question is what they actually catch. The block below
**models** twenty-four observation windows and the eighteen violations recorded in them, applies
the checks to the set, then measures what stands in for what cannot be checked, against the same
missed violations.

```js
// check.mjs — generates checks from the leaves, runs them against a known set of violations,
// then measures what stands in for the ones that cannot be checked
import { LEAF } from "./tree/tree.mjs";

// QA2: 18 violations recorded across 24 observation windows in two quarters (fiction). The third field is
// whether the violation reached the observation stream: G4's violation happened through a path that never gets
// logged. The fourth field is how many windows the violation lasted; structural flaws persist until fixed (99).
const VIOLATION = [
  [2, "B1", true, 1], [3, "G3", true, 4], [4, "E3", false, 20], [5, "S1", true, 1],
  [6, "B5", true, 2], [7, "G6", false, 99], [8, "E1", true, 1], [9, "B7", true, 3],
  [10, "T1", false, 1], [11, "G4", false, 99], [12, "S4", false, 99], [13, "B1", true, 1],
  [14, "S5", true, 6], [15, "E4", true, 1], [16, "S2", false, 10], [17, "G1", true, 5],
  [18, "B8", false, 99], [20, "S3", true, 8],
];
const WINDOW = 24;

// QA3: noise model. Each automatic leaf's window value is drawn from a uniform distribution
// around the threshold; the threshold sits near the upper end of the distribution, so windows
// cross it even without a violation. The generator is hand-written, the seed is visible.
const generator = (seed) => () => {
  seed = (seed + 0x6d2b79f5) | 0;
  let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
const random = generator(41307);

const AUTO = LEAF.filter((y) => y.source === "auto");
const find = (code) => LEAF.find((y) => y.code === code);
const draw = (y, r) => (y.threshold === 0 ? (r < 0.06 ? 1 : 0)
  : y.threshold * (y.direction === "high" ? 0.95 + r * 0.65 : 0.4 + r * 0.65));
const violationValue = (y) => (y.threshold === 0 ? 2 : y.threshold * (y.direction === "high" ? 0.8 : 1.3));

const record = [];
for (let p = 1; p <= WINDOW; p += 1) {
  const observation = {};
  for (const y of AUTO) observation[y.observation] = draw(y, random());
  const v = VIOLATION.find(([n]) => n === p);
  if (v && v[2]) observation[find(v[1]).observation] = violationValue(find(v[1]));
  record.push({ p, observation, violation: v ? v[1] : null });
}

// The check is generated from the leaf itself: it takes no information beyond the observation field, the threshold, and the direction.
const check = (y) => (o) => (y.direction === "high" ? o[y.observation] < y.threshold : o[y.observation] > y.threshold);
const firing = [];
for (const r of record)
  for (const y of AUTO) if (check(y)(r.observation)) firing.push([r.p, y.code, r.violation === y.code]);

const caught = firing.filter(([, , d]) => d);
const falseAlarm = firing.filter(([, , d]) => !d);
const missed = VIOLATION.filter(([p, code]) => !caught.some(([q, k]) => q === p && k === code));
console.log(`${WINDOW} windows, ${VIOLATION.length} known violations, ${AUTO.length} executable checks`);
console.log(`caught ${caught.length}, missed ${missed.length}, false alarm ${falseAlarm.length}\n`);

const alarm = {};
for (const [, code] of falseAlarm) alarm[code] = (alarm[code] || 0) + 1;
const ranked = Object.entries(alarm).sort((a, b) => b[1] - a[1]);
console.log(`${ranked.length} checks fired at least one false alarm; the three noisiest: ` +
  ranked.slice(0, 3).map(([k, n]) => `${k} (${n})`).join(", "));

// QA4: team behavior. A check that produces more than two false alarms in two quarters gets disabled.
const disabled = ranked.filter(([, n]) => n > 2).map(([k]) => k);
const remaining = caught.filter(([, code]) => !disabled.includes(code));
console.log(`disable threshold 2: disabled checks ${disabled.length} (${disabled.join(", ")}); ` +
  `caught afterward ${remaining.length}, missed ${VIOLATION.length - remaining.length}, ` +
  `false alarm ${falseAlarm.filter(([, code]) => !disabled.includes(code)).length}`);
const table = AUTO.reduce((s, y) => s + `${y.observation}${y.unit}${y.threshold}${y.direction}`.length, 0);
console.log(`check cost: rule body 1 line (same for all), threshold table ` +
  `${AUTO.length} lines / ${table} characters, run ${WINDOW * AUTO.length} comparisons, ` +
  `filled observation fields ${WINDOW * AUTO.length}`);

// QA5: two arrangements stand in for what cannot be checked; sessions run at window 12 and
// 24. Five leaves with a threshold but no log get a measurement session (6 person-hours), five
// leaves that cannot tie to a number get a review sampling 35% of the field (4 person-hours).
// A missed violation on an automatic leaf falls under neither arrangement.
const SESSION = [12, 24], SAMPLE = 0.35, HOURS = { measure: 6, review: 4 };
const arrangement = (y) => (y.source === "manual" ? "measure" : y.threshold === null ? "review" : "out-of-scope");
const second = generator(9152);

console.log(`\n${"window".padStart(7)}  ${"code".padEnd(6)}${"stands in".padEnd(14)}` +
  `${"result".padEnd(11)}delay`);
let found = 0, delay = 0;
for (const [p, code, , duration] of missed) {
  const d = arrangement(find(code));
  let s = SESSION.find((n) => n >= p && n <= p + duration &&
    (d === "measure" || (d === "review" && second() < SAMPLE)));
  if (d === "out-of-scope") s = undefined;
  if (s) { found += 1; delay += s - p; }
  console.log(`${String(p).padStart(7)}  ${code.padEnd(6)}${d.padEnd(14)}` +
    `${(s ? "caught" : "missed").padEnd(11)}${s ? s - p : "-"}`);
}
const scope = LEAF.filter((y) => y.source !== "auto");
const hours = scope.reduce((s, y) => s + HOURS[arrangement(y)], 0) * SESSION.length;
console.log(`\n${missed.length - 1} of the ${missed.length} missed violations fall within the standing-in arrangement: ` +
  `${found} caught, ${missed.length - 1 - found} missed; one is in neither arrangement`);
console.log(`average delay ${(delay / found).toFixed(1)} windows ` +
  `(0 for an executable check), cost ${hours} person-hours, ` +
  `${(hours / found).toFixed(0)} person-hours per catch`);
```

```
24 windows, 18 known violations, 20 executable checks
caught 11, missed 7, false alarm 23

15 checks fired at least one false alarm; the three noisiest: B1 (3), B4 (3), G3 (2)
disable threshold 2: disabled checks 2 (B1, B4); caught afterward 9, missed 9, false alarm 17
check cost: rule body 1 line (same for all), threshold table 20 lines / 325 characters, run 480 comparisons, filled observation fields 480

 window  code  stands in     result     delay
      4  E3    measure       caught     8
      7  G6    review        missed     -
     10  T1    measure       missed     -
     11  G4    out-of-scope  missed     -
     12  S4    review        caught     12
     16  S2    measure       caught     8
     18  B8    review        caught     6

6 of the 7 missed violations fall within the standing-in arrangement: 4 caught, 2 missed; one is in neither arrangement
average delay 8.5 windows (0 for an executable check), cost 100 person-hours, 25 person-hours per catch
```

11 of the eighteen violations get caught, seven are missed. Three of the missed ones sit on
leaves that have a threshold but no log, three on leaves that cannot tie to a number: those six
are an expected loss, the tree had already flagged them. The seventh is different: the
eleventh window's violation happened on a leaf with a written check and a zero threshold, and
still got missed — it never passed through that leaf's observation field.
**A check's scope is only as wide as the observation field feeding it** — a correctly written threshold makes the check look correct.

23 false alarms, more than double the caught count; fifteen checks fired without cause. This
is not the check's fault — it is where the threshold sits relative to the distribution: near the
upper end of observed values, it gets crossed without a violation. In the model, the team
disables any check producing more than two false alarms per two quarters; two get disabled,
dropping the caught count from 11 to 9. **A false alarm's cost is not the review time it burns — it is the violations the disabled
check will not catch afterward.** This triple — caught, missed, false alarm — matches the false-pass/false-fail pair in the
testing courses: what is measured here is the architectural rule, not a test case.

The check's own cost sits in four line items: the rule body is one line, identical across all
twenty leaves since it is generated from the leaf's threshold; the threshold table is 20 lines
and 325 characters; the run is 480 comparisons; the fourth and largest item is filling 480
observation fields. What makes the rule expensive is not its text but the data feeding it.

## What Stands In for What Cannot Be Checked

The ten leaves that cannot be checked are not left idle: the five with a threshold but no log get
a quarterly measurement session, the other five get a review. Six of the
seven missed violations fall within these two arrangements; four get caught, two are missed. One
lasted a single window and had closed by the twelfth window's session; nothing was left to
find. The sampling review missed a flaw too: it covers a third of the field,
and the flaw fell outside it.

Average delay is 8.5 windows; for an executable check it is zero, showing up in the window it
happens. The cost is 100 person-hours over two quarters, 25 per violation caught; twenty checks
caught 11 violations in that span at no human cost. The manual arrangement
pays off too: no false alarms, and the person deciding sees the context. What is left is one violation nobody
covers; the tree names it too — not an uncovered leaf but an **uncovered path**.

## Summary

- The quality attribute tree consists of a root, sub-attributes, and leaves; the measure sits
  only at the leaf, and 25 of thirty leaves tie to a number, five do not.
- Tying to a number is not the same as being checkable: 20 of the 25 numbered leaves turn into a
  check, five need a manual measurement despite having a written threshold, because they have no
  observation.
- Twenty checks catch 11 of eighteen violations, miss seven, and produce 23 false alarms; one is
  missed because it never reached the observation field despite having a written check.
- The cost of a false alarm is measured: disabling the two checks that produce more than two
  alarms drops the caught count from 11 to 9.
- What stands in for what cannot be checked catches four of the six covered missed violations at
  8.5 windows of delay and 100 person-hours; the cost of an executable check is one line of rule
  and 480 observation fields.

## Next Step

Once the tree is built, every leaf looks checked on its own, by its own threshold. The next
lesson breaks that picture: the leaves are not independent of each other. A decision lowering
connection pool saturation lengthens failover time; one raising test coverage overruns the bulk
upload window. This relationship is not found by guessing but by measuring — a
tension matrix gets built, decisions that raise one attribute are applied in turn, and the drop
measured in the others is counted after each one. What comes out is how
many pairs conflict, which attribute is suppressed the most, and whether there is an attribute no
decision can improve.
