Software Architecture · Modularity
Instability, Abstractness, and the Main Sequence Metric
How afferent and efferent coupling produce an instability score, why stable packages should be abstract, and where the distance-from-the-main-sequence number misleads.
A package that forty other packages depend on is expensive to change, because every change is a potential break in forty places. That is not a property of its code; it is a property of its position in the graph. The instability metric turns the position into a number, and the abstractness metric turns the package’s internal composition into another, so that the two can be compared. The comparison is useful. The number it produces is often treated as a grade, which it is not.
The two inputs
Afferent coupling, written Ca, counts the classes outside a package that depend on classes inside it. Efferent coupling, Ce, counts the classes inside the package that depend on classes outside. Both are counted per class, not per package, which is the first place the metric departs from intuition: a package imported by one class in forty different packages has a Ca of forty, and so does a package imported by forty classes in one package.
Instability is defined as
I = Ce / (Ca + Ce)
with the convention that a package with no coupling at all has I = 0. The value lies in [0, 1]. A package with I = 0 depends on nothing and is depended on by others: it is stable, in the sense that nothing outside it can force it to change, and everything inside it can force changes outside. A package with I = 1 depends on others and has no dependents: it is unstable, free to change because nothing depends on it, and forced to change whenever its dependencies do.
The naming is inverted relative to ordinary usage. A “stable” package here is one that is hard to modify without consequences, not one that stays the same. Stability in this sense is a measure of responsibility: the higher the fan-in, the more the package owes its dependents.
Why stability and abstractness should be related
Abstractness is the ratio of abstract types to total types in a package: A = (abstract classes and interfaces) / (total classes), also in [0, 1]. A package of pure interfaces has A = 1; a package of concrete classes with no abstraction has A = 0.
The design argument connects the two. A stable package with A = 0 is a concrete implementation that many modules depend on directly. Any change to its behaviour or its shape propagates. The conventional remedy is to make the stable package abstract — interfaces, abstract classes, protocols — so that dependents bind to a specification that can have multiple implementations and can change implementation without changing the specification. Conversely, an unstable package with A = 1 is a collection of interfaces that nothing depends on and that depend on everything, which is a layer of indirection with no beneficiary.
Plotting I on the horizontal axis and A on the vertical, the two desirable regions are the ends of the line from (I = 0, A = 1) to (I = 1, A = 0). That line is called the main sequence. Distance from it is D = |A + I - 1|, which is zero on the line and one at the opposite corners: (0, 0), the stable concrete package, and (1, 1), the unstable abstract package. Martin labels the first the zone of pain and the second the zone of uselessness.
A worked computation
The arithmetic is simple enough that the metric’s behaviour is easier to see on a small example than in a definition.
def metrics(ca: int, ce: int, abstract: int, total: int) -> tuple[float, float, float]:
instability = ce / (ca + ce) if ca + ce else 0.0
abstractness = abstract / total if total else 0.0
return instability, abstractness, abs(abstractness + instability - 1)
Suppose a package pricing contains twelve types: three interfaces, one abstract class, eight concrete classes. Eight classes outside the package reference types inside it; the package itself references six classes elsewhere.
Then Ca = 8, Ce = 6, I = 6/14 ≈ 0.43. Abstract types are 4 of 12, so A ≈ 0.33. The sum A + I ≈ 0.76, so D ≈ 0.24. The package is slightly more concrete than its position warrants: it is nearly balanced in coupling, and about a third abstract. Whether that is a problem depends on which of its eight concrete classes are the ones being depended on. If the dependents use only the interfaces, the abstractness of the surface is higher than the package average and the metric understates the design quality. If they use concrete classes directly, the metric is right and the package is a candidate for extracting an interface layer.
That conditional is the heart of the critique. The metric averages over types, but dependents attach to specific types. A package with one heavily used concrete class and eleven unused abstract ones scores A ≈ 0.92 and looks healthy while the actual coupling is entirely concrete.
The metric assumes a direction of change
The main sequence encodes a claim: stable things should be abstract because abstraction absorbs change. The claim holds when the stable package is a policy or a specification whose implementation varies. It does not hold when the stable package is a data type whose shape is genuinely fixed. A Money class with an amount and a currency, depended on by everything, has A = 0 and I = 0, and moving it toward the main sequence would mean inventing an interface for a value whose representation is not going to change. The distance metric flags it; the design is fine.
The reverse case is a stable package that is abstract for the wrong reason: an interface layer introduced to satisfy a metric, with one implementation and no prospect of a second. Its A is high, its I is low, its D is small, and it has added a dispatch boundary that buys nothing. This is one way the metric is gamed, and it is why the number is more useful as a cue to ask a question than as a pass/fail threshold.
What the inputs hide
Edge weight. Ca and Ce count classes, so an import of a constant is worth as much as a call into a stateful API. The knowledge difference between those edges — one is a compile-time literal, the other is a behavioural dependency — disappears in the sum. Two packages with identical I can have very different change costs.
Cycles. In a cycle, each package’s Ca includes the other, so both look more stable than they are, and the condensation that would reveal the real unit is not performed. The dependency graph analysis has to come first; metrics computed on a cyclic graph attribute stability to packages that cannot actually be held still.
Granularity of abstraction. An interface with twelve methods and one implementation is counted as one abstract type, the same as a one-method interface with three implementations. Weighting by method count or by implementation count changes the picture and is not part of the standard definition.
Generated and framework code. A package with generated DTOs and framework base classes can have a large type count and an abstractness driven by constructs nobody designed. Excluding generated sources is a judgement call, and any comparison across packages has to make the same call consistently.
Language differences. In Go, an interface is a type; in Rust, a trait is not a class; in Python, an abstract base class is a runtime object. The denominator “total classes” is not comparably defined across languages, so cross-language comparison of A is meaningless even when the formulas are identical.
Using the numbers without obeying them
Three uses survive the caveats.
Rank packages by Ca to find change multipliers. This does not need abstractness at all: the packages with the most dependents are the ones where a change is most expensive, and they are where interface stability matters most. The ranking is unaffected by the weighting problems because it only uses one input.
Find packages in the zone of pain and ask why. A stable concrete package is either a value type that is genuinely fixed, in which case it needs documentation rather than refactoring, or an implementation that leaked into a shared position, in which case the fix is to extract the interface that dependents should have used. The metric distinguishes the two cases only by inspection.
Track D over time for a small set of packages rather than scoring the whole system. A rising D in a package that used to sit near the line means abstraction and coupling have drifted apart, which is a real signal. A single snapshot of D across two hundred packages is mostly noise from type-counting artefacts.
The deeper point is that the metrics describe a snapshot of a structure whose purpose is to absorb change, and they contain no information about which changes are coming. That information lives in the design rationale: which decisions were hidden, and why. Information hiding supplies the intent; the metrics check whether the structure still matches it. When they disagree, the intent is usually right and the number is measuring the wrong thing.
References
- Agile Software Development: Principles, Patterns, and Practices — Robert C. Martin, Prentice Hall, 2002
- Clean Architecture: A Craftsman's Guide to Software Structure and Design — Robert C. Martin, Prentice Hall, 2017
- On the Criteria To Be Used in Decomposing Systems into Modules — D. L. Parnas, Communications of the ACM 15(12), 1972