---
title: 'Component Coupling Principles'
source: 'https://academia.sh/en/courses/design-principles/component-coupling-principles'
course: 'Design Principles'
language: en
updated: '2026-08-23T07:01:16+00:00'
license: 'CC BY-SA 4.0'
---

# Component Coupling Principles

Measuring the direction of the bond between components: computing the instability metric from fan-in and fan-out, counting edges that do not flow in the stable direction, deriving a release order with topological sort, and showing how a single import into the stable core makes ordering impossible.

The previous lesson pulled the component boundary from files that change together and compared
two splits by republish count. Those measures looked **inside** components: which files should
stand together. Edges between components were counted, but their direction was not questioned.

An inter-component edge is the component-scale counterpart of the coupling measured for module
pairs in the Coupling and Cohesion topic. The difference is direction: there, the strength of
the bond was counted; here, which way it points will be counted. There is a criterion for which
direction an edge should point, and it is again about change: a component bound to a frequently
changing one gets dragged along behind those changes even if it never changes itself. This
lesson computes a stability measure for every component, counts whether links flow in the right
direction, and shows what stability buys in the release order.

## Stability Is Something Measurable

A component's stability is not that it never changes, it is that **changing it is hard**. Two
numbers determine this: how many components are bound to it (**fan-in**) and how many
components it is bound to (**fan-out**). A component with a high fan-in is hard to change,
because changing it breaks others. A component with a high fan-out is easy to change, because
changing it breaks no one — but that same component is exposed to being affected by others'
changes.

The two reduce to a single ratio. The **instability metric** is the ratio of fan-out to total
links:

$$I = \frac{\text{fan-out}}{\text{fan-in} + \text{fan-out}}$$

A component with $I = 0$ is the most stable: things bind to it, it binds to nothing. A
component with $I = 1$ is the most unstable: nothing is bound to it, it is bound to others. The
measure reports position, not quality; a library needs components at both ends.

The **stable dependencies principle** is built on top of this measure: an edge must flow in the
direction where $I$ **drops**. A stable component binding to an unstable one turns its own
stability into a fiction — the component looks closed to change, but it gets republished every
time the side it is bound to changes.

## Three Graphs to Measure

The two splits from the previous lesson give the same library's two component graphs. A third
graph is produced by adding a single import to the second: the money component starts
formatting amounts by zone and imports the tariff component.

```js
// component-graph.mjs — the same library's three component graphs
export const DIRECTORY = {
  components: ["fee", "zone", "carrier", "route", "delivery", "shared"],
  edges: [["fee", "zone"], ["fee", "shared"], ["carrier", "fee"],
    ["route", "carrier"], ["delivery", "route"]],
};

export const COCHANGE = {
  components: ["tariff", "commercial-rule", "network", "tracking", "money"],
  edges: [["commercial-rule", "tariff"], ["commercial-rule", "money"], ["commercial-rule", "network"],
    ["tariff", "money"], ["tracking", "network"]],
};

export const BROKEN = {
  components: COCHANGE.components,
  edges: [...COCHANGE.edges, ["money", "tariff"]],
};
```

The measuring tool computes two things. First, the instability metric and the edges not
flowing in the stable direction. Second, the **release order**: a component can be released
only after everything it is bound to has been released. This is the component-level
counterpart of the topological sort established in the Data Structures course; if a component
is left unsorted, there is a cycle in the graph.

```js
// stability.mjs — instability metric, stable-direction violations, and release order
export function measure(components, edges) {
  const fanIn = Object.fromEntries(components.map((c) => [c, 0]));
  const fanOut = Object.fromEntries(components.map((c) => [c, 0]));
  for (const [a, b] of edges) { fanOut[a] += 1; fanIn[b] += 1; }
  const I = Object.fromEntries(components.map((c) => {
    const total = fanIn[c] + fanOut[c];
    return [c, total === 0 ? 0 : fanOut[c] / total];
  }));
  return { fanIn, fanOut, I, violations: edges.filter(([a, b]) => I[a] < I[b]) };
}

export function releaseOrder(components, edges) {
  const remaining = new Set(components);
  const order = [];
  for (;;) {
    const ready = [...remaining]
      .filter((c) => edges.every(([a, r]) => a !== c || !remaining.has(r))).sort();
    if (ready.length === 0) return { order, remaining: [...remaining].sort() };
    for (const c of ready) { order.push(c); remaining.delete(c); }
    if (remaining.size === 0) return { order, remaining: [] };
  }
}
```

```js
// stability-report.mjs — reports all three graphs with the same measures
import { DIRECTORY, COCHANGE, BROKEN } from "./component-graph.mjs";
import { measure, releaseOrder } from "./stability.mjs";

for (const [name, graph] of [["directory", DIRECTORY], ["co-change", COCHANGE], ["broken", BROKEN]]) {
  const { fanIn, fanOut, I, violations } = measure(graph.components, graph.edges);
  console.log(`${name} graph — ${graph.components.length} components, ${graph.edges.length} edges`);
  for (const c of [...graph.components].sort((x, y) => I[x] - I[y] || x.localeCompare(y))) {
    console.log(`  ${c.padEnd(16)} fan-in ${fanIn[c]}  fan-out ${fanOut[c]}  I = ${I[c].toFixed(2)}`);
  }
  console.log(`  stable-direction violations = ${violations.length} / ${graph.edges.length}` +
    (violations.length ? `  ${violations.map(([a, b]) => `${a} -> ${b}`).join(", ")}` : ""));
  const { order, remaining } = releaseOrder(graph.components, graph.edges);
  console.log(`  release order: ${order.join(" -> ")}` +
    (remaining.length ? `  || unorderable: ${remaining.join(", ")}` : ""));
}
```

```
directory graph — 6 components, 5 edges
  shared           fan-in 1  fan-out 0  I = 0.00
  zone             fan-in 1  fan-out 0  I = 0.00
  carrier          fan-in 1  fan-out 1  I = 0.50
  route            fan-in 1  fan-out 1  I = 0.50
  fee              fan-in 1  fan-out 2  I = 0.67
  delivery         fan-in 0  fan-out 1  I = 1.00
  stable-direction violations = 1 / 5  carrier -> fee
  release order: shared -> zone -> fee -> carrier -> route -> delivery
co-change graph — 5 components, 5 edges
  money            fan-in 2  fan-out 0  I = 0.00
  network          fan-in 2  fan-out 0  I = 0.00
  tariff           fan-in 1  fan-out 1  I = 0.50
  commercial-rule  fan-in 0  fan-out 3  I = 1.00
  tracking         fan-in 0  fan-out 1  I = 1.00
  stable-direction violations = 0 / 5
  release order: money -> network -> tariff -> tracking -> commercial-rule
broken graph — 5 components, 6 edges
  network          fan-in 2  fan-out 0  I = 0.00
  money            fan-in 2  fan-out 1  I = 0.33
  tariff           fan-in 2  fan-out 1  I = 0.33
  commercial-rule  fan-in 0  fan-out 3  I = 1.00
  tracking         fan-in 0  fan-out 1  I = 1.00
  stable-direction violations = 0 / 6
  release order: network -> tracking  || unorderable: commercial-rule, money, tariff
```

## Where the Direction Is Wrong

In the directory graph, one of five edges does not flow in the stable direction: `carrier ->
fee`. The carrier component's instability is 0.50, the fee component's is 0.67. The link points
toward the more unstable one.

What this means concretely: because the route component is bound to carrier, and carrier to
fee, every commercial-rule change reaching the fee component walks all the way to the route
component. The fee component was the most frequently touched spot in the previous lesson's
log; the most-changing component has settled in the middle of the chain.

In the co-change graph, that count is zero. All five edges flow from unstable to stable: the two
most unstable components (`commercial-rule` and `tracking`) feed into no component, and the two
most stable ones (`network` and `money`) bind to nothing. The measure confirms the previous
lesson's split with an independent criterion.

## What Stability Buys

If the stable direction were only an aesthetic preference, it would not be worth measuring. What
it buys shows up in the release order: in the co-change graph, the five components release in
the order `money -> network -> tariff -> tracking -> commercial-rule`, guaranteeing that
everything each component is bound to was released before it. Which components need retesting
once a new version ships is also read from the same order.

The condition for the order to exist is the **acyclic dependencies principle**: the component
graph must not contain a cycle. If there is a cycle, there is no release order, because every
component in the cycle is waiting for another one to be released first.

## A Single Import Into the Stable Core

The broken graph shows exactly this. The one edge added is `money -> tariff`. The results:

The money component's instability went from 0.00 to 0.33. The library's most stable part is now
bound to the tariff table; every time the price list is refreshed, the money component gets
republished too. The tariff component symmetrically dropped from 0.50 to 0.33 — the number went
down, but the situation did not improve, because the two components are now entangled in each
other.

The stable-direction violation count came out **zero**. Because the two components' instability
is equal, no edge trips the "does not flow from unstable to stable" criterion. This is proof
that the instability metric alone is not enough: a cycle does not violate the measure, it makes
the measure **meaningless**.

The release order does not hide the situation, though. Only two of the five components could be
ordered; `money`, `tariff`, and `commercial-rule` came out unorderable. `commercial-rule` is on
the list even though it is not inside the cycle: every component bound to a cycle also becomes
unorderable. A single import line in one component made a third component, bound to it from a
distance, impossible to release.

## Breaking the Cycle

There are two known ways to remove a cycle, and both are moves already established earlier.

The first is reversing the direction: instead of importing the tariff, the money component takes
the information it needs for formatting as a parameter. This is the component-scale counterpart
of the first lesson's move — the rule does not import the table, the table is given to the rule.
The dependency inversion item of SOLID and the layer rule from M16/K04 describe this same move.

The second is inserting a component in between: a shared need of two components is split off
into a third component, and both bind to it. The cycle is broken because both edges now point
the same way.

Once the first way is applied, the graph becomes the co-change graph again: `money` returns to
$I = 0$, the edge count drops from six to five, and all five components get ordered. All three
measures recover at once.

## Summary

- A component's instability is computed as
  $I = \text{fan-out} / (\text{fan-in} + \text{fan-out})$; 0 marks the most stable
  position, 1 the most unstable.
- The stable dependencies principle asks edges to flow in the direction where $I$ drops; 1 of 5
  edges did not flow this way under the directory split, 0 of 5 under the co-change split.
- Stability's payoff is the release order: in the co-change graph, all five components can be
  released in a single order, and once a version ships, which components need retesting is read
  from that same order.
- A single import added to the stable core (`money -> tariff`) raised the money component's
  instability from 0.00 to 0.33 and made three of the five components unorderable.
- The same broken graph produced zero stable-direction violations; the instability metric does
  not catch a cycle — a cycle makes the measure itself meaningless.

## Next Step

The release order said a cycle **exists**, but not where it was: which of the three unorderable
components is inside the cycle and which is only on the list because it is bound to it could not
be read from the output. Also, every measure in this section sat on a component graph written by
hand; in a real codebase, the graph is extracted from import lines. The next lesson writes a tool
that reads the import graph from source files, lists cycles by name with a depth-first search,
and counts imports violating the layer rule; it first runs the tool on a broken version, then
shows both numbers dropping to zero on the fixed version.
