---
title: 'Style Selection and Trade-off Analysis'
source: 'https://academia.sh/en/courses/architectural-styles/style-selection-and-trade-off-analysis'
course: 'Architectural Styles'
language: en
updated: '2026-08-23T07:01:09+00:00'
license: 'CC BY-SA 4.0'
---

# Style Selection and Trade-off Analysis

Turning a style decision into a measurable defense: measuring the same three rules on a common footing across two boundary arrangements, computing pass/fail against the threshold of three quality scenarios, showing that no arrangement passes all three thresholds at once, and finding the threshold that flips the decision.

The five deployment arrangements left five separate sets of numbers: files published together,
boundary crossing, bytes crossing the boundary, units left standing when one unit stops, and
files edited for a new capability. No arrangement led on all of these measures; every gain grew
another number. This lesson takes up how a choice gets made in that situation, and what its
defense rests on.

Quality attribute and quality scenario were established in the Quality and Testing Fundamentals
course's Quality Attributes lesson. They are not defined here; they are used as decision tools.

## The Three Parts That Carry a Decision

A style defense has three parts. First, the **scenario**: which quality, under which stimulus, in
which environment. Second, the **threshold**: the accepted value on that quality's response
measure. Third, the **measured value**: the number the arrangement actually gets. Missing any one
of the three leaves a preference, not a defense. "Microservices are more flexible" is missing all
three: which change, which threshold, and which measure stays unclear.

Trade-off comes from the same shape: an arrangement grows another measure's value to pass a
threshold, and the trade-off is these two numbers standing side by side in the same decision. A
measurement comparing two arrangements therefore produces not a single number but a vector.

## Common Footing

Comparison requires a common footing: the same rules, the same input, the same counting
procedure — otherwise the measured difference comes from the two implementations' detail, not the
style. This is why the three rules sit in a single file that both arrangements use; only the
boundary placement changes.

```js
// decision/rule.mjs — three rules: both arrangements use the same rules, only the boundary placement changes
const TARIFF = { tier: [[1, 3000], [5, 4800], [20, 9600]], zone: { "34": 100, "06": 115, "35": 125 }, minimum: 2500 };
const TREE = { "34": ["34"], "06": ["34", "06"], "35": ["34", "41", "35"] };

export const RULE = {
  fee(s) {
    const tier = TARIFF.tier.find(([k]) => s.weight <= k) ?? [0, 9600];
    const base = Math.max(TARIFF.minimum, Math.round((tier[1] * TARIFF.zone[s.zone]) / 100));
    return { net: base - Math.round(base * Math.min(s.rate, 0.4)) };
  },
  plan(s) {
    const route = TREE[s.zone] ?? ["34"];
    return { day: route.length, carrier: route.length > 2 ? "MT" : "AN" };
  },
  volume(s, state) {
    state.set(s.contractNo, (state.get(s.contractNo) ?? 0) + s.weight);
    return { volume: [...state.values()].reduce((t, a) => t + a, 0) };
  },
};
```

An arrangement is a list saying which rule stands in which deployment unit. In the one-unit
arrangement, all three rules share a unit with no boundary between them; in the three-unit
arrangement, each rule has its own unit and the body serializes at every boundary. The runner
measures both with the same counters and carries the option to stop a unit.

```js
// decision/arrangement.mjs — runs the same three rules in two arrangements: one deployment unit and three separate units
import { RULE } from "./rule.mjs";

export const ARRANGEMENT = {
  "one unit": [["fee", "plan", "volume"]],
  "three units": [["fee"], ["plan"], ["volume"]],
};

export function run(units, shipments, down = null) {
  const measure = { crossing: 0, transform: 0, bytes: 0, answered: 0, standing: 0, last: {} };
  const state = new Map();
  for (const s of shipments) {
    const result = {};
    for (const unit of units) {
      if (unit.includes(down)) continue;
      const text = units.length === 1 ? null : JSON.stringify(s);   // the body serializes if there is a boundary
      if (text !== null) measure.transform += 2, measure.bytes += text.length;
      measure.crossing += 1;
      for (const name of unit) Object.assign(result, RULE[name](text === null ? s : JSON.parse(text), state));
    }
    if ("net" in result) measure.answered += 1;                     // a request is unanswered without a fee
    measure.last = result;
  }
  measure.standing = units.filter((b) => b.includes(down) === false).length;
  return measure;
}
```

## Three Scenarios and a Threshold

The three scenarios come from three separate quality families, each reduced to a single response
measure. Maintainability asks how many rules must be published together when the tariff rule
changes, and accepts at most 1. Performance efficiency asks how many bytes cross the boundary for
three shipments, and accepts at most 100. Reliability asks how many requests are answered while
the volume unit is down, and requires at least 3. The script applies the three thresholds to both
arrangements, then moves each threshold to the arrangement's measured value to find where the
decision flips.

```js
// decision/measure.mjs — measures the two arrangements against the threshold of three quality scenarios and searches for the threshold that flips the decision
import { ARRANGEMENT, run } from "./arrangement.mjs";

const SHIPMENT = [
  { id: "G1", weight: 4, zone: "35", contractNo: "S7", rate: 0.15 },
  { id: "G2", weight: 1, zone: "34", contractNo: "S7", rate: 0 },
  { id: "G3", weight: 12, zone: "06", contractNo: "S9", rate: 0.25 },
];
const SCENARIO = [
  { name: "S1 maintainability", metric: "rules published together", threshold: 1, atMost: true },
  { name: "S2 performance efficiency", metric: "bytes crossing the boundary", threshold: 100, atMost: true },
  { name: "S3 reliability", metric: "requests answered", threshold: 3, atMost: false },
];
const FIELD = ["deployment unit", "rules published together", "boundary crossing", "transform point",
  "bytes crossing the boundary", "requests answered", "units left standing"];

const measurement = Object.entries(ARRANGEMENT).map(([name, units]) => {
  const full = run(units, SHIPMENT);
  const degraded = run(units, SHIPMENT, "volume");
  return {
    name, last: full.last,
    "deployment unit": units.length,
    "rules published together": units.find((b) => b.includes("fee")).length,
    "boundary crossing": full.crossing,
    "transform point": full.transform,
    "bytes crossing the boundary": full.bytes,
    "requests answered": degraded.answered,
    "units left standing": `${degraded.standing}/${units.length}`,
  };
});

console.log(`${"measure".padEnd(29)}${measurement.map((o) => o.name.padStart(12)).join("")}`);
for (const a of FIELD) console.log(`${a.padEnd(29)}${measurement.map((o) => String(o[a]).padStart(12)).join("")}`);
for (const o of measurement) console.log(`${o.name.padEnd(12)} last shipment's result = ${JSON.stringify(o.last)}`);

const passes = (o, s) => (s.atMost ? o[s.metric] <= s.threshold : o[s.metric] >= s.threshold);
for (const s of SCENARIO) {
  const reading = measurement.map((o) => `${o.name} = ${o[s.metric]} ${passes(o, s) ? "passes" : "fails"}`);
  console.log(`${s.name}: ${s.metric} ${s.atMost ? "<=" : ">="} ${s.threshold} -> ${reading.join(", ")}`);
}
for (const o of measurement) console.log(`${o.name.padEnd(12)} scenarios passed = ${SCENARIO.filter((s) => passes(o, s)).length}/3`);
console.log(`arrangements passing all three = ${measurement.filter((o) => SCENARIO.every((s) => passes(o, s))).length}`);

for (const s of SCENARIO) {
  for (const o of measurement.filter((x) => passes(x, s) === false)) {
    console.log(`${s.name}: unless the threshold is ${o[s.metric]} instead of ${s.threshold}, "${o.name}" does not pass`);
  }
}
for (const s of SCENARIO) {
  const remaining = SCENARIO.filter((x) => x !== s);
  const passing = measurement.filter((o) => remaining.every((x) => passes(o, x))).map((o) => o.name);
  console.log(`if ${s.name} is dropped, arrangements passing the remaining two = ${passing.join(", ") || "none"}`);
}
```

```sh
node decision/measure.mjs
```

```
measure                          one unit three units
deployment unit                         1           3
rules published together                3           1
boundary crossing                       3           9
transform point                         0          18
bytes crossing the boundary             0         570
requests answered                       0           3
units left standing                   0/1         2/3
one unit     last shipment's result = {"net":8280,"day":2,"carrier":"AN","volume":17}
three units  last shipment's result = {"net":8280,"day":2,"carrier":"AN","volume":17}
S1 maintainability: rules published together <= 1 -> one unit = 3 fails, three units = 1 passes
S2 performance efficiency: bytes crossing the boundary <= 100 -> one unit = 0 passes, three units = 570 fails
S3 reliability: requests answered >= 3 -> one unit = 0 fails, three units = 3 passes
one unit     scenarios passed = 1/3
three units  scenarios passed = 2/3
arrangements passing all three = 0
S1 maintainability: unless the threshold is 3 instead of 1, "one unit" does not pass
S2 performance efficiency: unless the threshold is 570 instead of 100, "three units" does not pass
S3 reliability: unless the threshold is 0 instead of 3, "one unit" does not pass
if S1 maintainability is dropped, arrangements passing the remaining two = none
if S2 performance efficiency is dropped, arrangements passing the remaining two = three units
if S3 reliability is dropped, arrangements passing the remaining two = none
```

## Reading the Numbers

The two arrangements' result for the last shipment is identical:
`{"net":8280,"day":2,"carrier":"AN","volume":17}`. This line says what the decision is *not*
about: correctness is the same in both, so the style choice rests on the numbers around it, not
on functional expectation.

The rest of the vector separates the two arrangements in opposite directions. In the one-unit
arrangement, boundary crossing is 3, transform point is 0, bytes is 0 — the three rules are
called in the same memory region, so there is no boundary cost. In exchange, rules published
together when the tariff changes is 3, and requests answered when volume is down drops to 0/3,
units left standing to 0/1: sharing a unit means one stopping stops all. In the three-unit
arrangement the first three numbers grow — crossing 9, transform 18, bytes 570, roughly 63 bytes
carried nine times per shipment — but rules published together falls to 1, requests answered
rises to 3/3.

These two lines relate to what the first and third lessons measured; new are the three lines
beneath them. Against the three thresholds, the one-unit arrangement passes 1/3, the three-unit
arrangement 2/3, and arrangements passing all three is 0 — not a measurement gap but the shape of
the answer: a contradictory threshold set satisfies no arrangement, and the decision turns into
ranking the thresholds.

Which threshold flips the ranking was also computed. S2 is the one: only dropping it produces an
arrangement passing the remaining two — the three-unit arrangement. Dropping S1 or S3 yields
nothing, because both are measures the one-unit arrangement fails. The same computation gives how
far the thresholds would have to move: the three-unit arrangement does not pass unless S2 rises
from 100 to 570, and the one-unit arrangement does not pass unless S1 falls from 1 to 3 and S3
from 3 to 0. A defense reads: "the three-unit arrangement was chosen because the S1 and S3
thresholds were kept and S2 was relaxed to 570 bytes."

Last, the unmeasured axis. The script knows only three measures; asked about a fourth quality, it
finds no number. An unmeasured axis carries no decision — it has no weight in the argument,
because which arrangement passes cannot be computed. This is why every lesson of the course
defined a measure.

## Summary

- A style defense has three parts: quality scenario, threshold, and measured value; missing one
  leaves a preference, not a defense.
- Comparison requires a common footing: both arrangements used the same three rules, three
  shipments, and counters, and produced an identical result for the last shipment.
- The one-unit arrangement kept boundary cost at zero (crossing 3, transform 0, bytes 0) but left
  rules published together at 3 and requests answered when volume is down at 0/3.
- The three-unit arrangement did the same work with 9 crossings, 18 transforms, and 570 bytes; in
  exchange, rules published together is 1, requests answered 3/3, units left standing 2/3.
- Arrangements passing all three thresholds came out 0; not a measurement gap but the result of a
  contradictory threshold set, which turns the decision into ranking the thresholds.
- The threshold that flips the decision was computed: only dropping S2 lets the three-unit
  arrangement pass the remaining two; it does not pass unless S2 rises from 100 to 570.

## Course Wrap-Up

The course took up the styles spread across three topics with a single question: when the same
shipment pricing and routing library is set up in that arrangement, which number shrinks and
which grows. The measures the lessons left behind are gathered in the decision table below.

| Lesson | Quality improved | Measured gain | Cost paid |
|---|---|---|---|
| Layered Architecture | maintainability | brought the open-listing read request from 4 layers to 2, and from 4 boundaries to 1 | the same request became satisfiable from two separate places |
| Hexagonal and Onion Architecture | maintainability | after the fix, ring edges dropped from 5 to 3 and both rules' violations to 0 | each rule is blind to the defect the other sees: hexagonal forbids 4 edges, onion forbids 6 |
| Clean Architecture | maintainability | passing the output model through dropped accessible names from 10 to 4 and JSON from 170 to 76 characters | a separate model type at the boundary and one transform point |
| Component-Based Architecture | maintainability | splitting by capability brought exported names from 8 to 6 and the co-release ratio from 0.39 to 0.27 | the single consumer that skipped the surfaces raised exported names from 6 to 10 |
| Microkernel and Plugin Architecture | maintainability | the kernel carries 0 plugin names instead of 4, and the import closure dropped from 5 files to 1 | the names moved to the composition root, whose closure is 6 files; the contract is checked at run time |
| Blackboard Architecture | extensibility | in seven of nine files, other unit names are 0; the fifth resolver added 1 file | the same result cost 3 rounds, 5 polls, and 7 reads; the first-writer-wins rule produced 3 distinct outcomes across 24 orderings |
| Client–Server | maintainability | the delivery side's known modules dropped from 3 to 1, names from 4 to 1 | the error moved from import time to run time; learning three changes took 12 questions, 9 of them blank |
| Peer-to-Peer | reliability | the critical unit is 1 in the star, 0 in the ring and full mesh | known neighbors rose from 10 to 30; the ring took 18 messages and 3 rounds |
| Model–View Families | maintainability | the view's known model names dropped from 5 to 0, the import closure from 2 to 1 | bytes crossing the boundary rose from 67 to 111, files changed from 1 to 2 |
| Pipes and Filters | testability | six of six steps ran standalone, the new step's file recognizes 0 field names | 17 of the 28 fields crossing the filter inputs passed through unread |
| Publish–Subscribe | maintainability | the publisher's known modules dropped from 3 to 1, the import closure from 4 to 2 files | "who handles this event" scanned 7 modules instead of 1 file |
| Message Queues and Streams | performance efficiency | the publisher spent 0 of 60 work units of its own time | the finish order broke in 3 places with a second consumer; 8 messages were delivered 9 times |
| Monolithic Architecture | performance efficiency | boundary crossing 4, transform point 0, bytes crossing the boundary 0 | a 1-file change republished 4 files; units left standing under the defect was 0/2 |
| Service-Oriented Architecture | compatibility | body formats to learn dropped to 1 | canonical field 10, request body 417 bytes; 3 files when `zone` changes |
| Microservice Architecture | reliability | files republished 1, 2 of 3 endpoints kept returning 200 when pricing stopped | 18 lines of server repetition, 4 outcome states, 3 contracts to learn |
| Event-Driven Architecture | maintainability | the third consumer edited 0 publisher lines; requests answered stayed 3/3 on a dropped consumer | transform point rose from 0 to 9, bytes from 0 to 470; the accumulator deviated by 12 |
| Serverless Architecture | maintainability | run lines 0, function files edited for a new composition 0 | setup rose from 2 to 6 on a cold run; module-state deviation 5, repository 13 accesses |

The table has a single reading: the right column is empty in no row. Every style improved one
quality in a measurable way and, in exchange, grew another number. A decision whose right column
looks empty is an unmeasured decision.

This course is the last of the Software Design and Architectural Principles curriculum's six
courses, and all six are ordered by growing scale. Clean Code measured lines and names;
Programming Paradigms measured how the same work is written across computation styles; Design
Principles measured the responsibility boundary of the module and the class; Design Patterns
measured the recurring arrangements of cooperating object sets; Domain-Driven Design measured an
application's domain model and context boundaries; this course measured the application itself
splitting into deployment units. At each step the unit of measure changed: line count to name
count, name count to import closure, import closure to boundary crossing, boundary crossing to
files published together and units left standing.

One more step remains, and there the question departs from this course's. This course compared
styles as a *design decision*: arrangements of the same library, boundaries modeled on a single
machine. The next curriculum, System Design and Distributed Systems, takes up the same units
under a real system's constraints — scale, latency, consistency, failure behavior. The 570 bytes
here become a network budget there, the 3/3 response ratio an availability target, and the state
the volume rule holds a consistency model. The style choice is still defended with the same three
parts; what changes is where the thresholds come from.
