skip to content

What are the limits of the Stable Dependencies Principle and its instability metric at scale, and how would you enforce dependency direction across many teams without turning the metric into a target?

level: principalimportance: nice to knowfreq 12%

answer

  1. Granularity moves every number
  2. Unweighted edges; volatility invisible
  3. Cycles break the inequality — ADP first
  4. Static graph misses HTTP, DB, topics
  5. Enforce allow-lists, report I; never a KPI

basics

~20 s

I depends entirely on how you carve components, counts every edge equally, and ignores volatility, cycles and runtime coupling. So enforce direction with explicit allowed-dependency rules in the build, and keep I as a review signal you never grade teams on.

solid answer

~50 s

SDP's inequality I(X) ≥ I(Y) is only as meaningful as the component boundaries you chose: recarve them and every number moves, so values are incomparable across repos, tools and time. It weights a one-line import the same as a deep API coupling, is blind to whether either component actually changes, breaks down entirely under dependency cycles, and misses runtime coupling — a service that calls another over HTTP, or two components sharing a database table, has coupling no import graph sees. At scale the useful mechanism is not the metric but an *explicit, checked contract*: declare each component's permitted dependencies (Spring Modulith's `allowedDependencies`, ArchUnit/dependency-cruiser rules, Bazel visibility, Nx tags, module systems) and fail the build on violation, with exceptions recorded as reviewed entries rather than silent drift. Layer SDP-flavoured review on top: a periodic report of edges where I increases, and a churn-versus-I overlay to find hot spots. Publishing I or D as a team KPI reliably produces gamed interfaces and fake components — Goodhart in one step.

go deeper

for a junior

Note the basics: the metric ignores how often things change and depends on how you split components.

for a middle

Add cycles and unweighted edges, and mention build-enforced module rules as the practical enforcement mechanism.

for a senior

Cover runtime coupling invisible to the import graph, third-party counting policy, and the churn × I overlay for triage.

for a principal

Frame the whole thing as governance: binding constraints as declared allowed-dependency rules with reviewed exceptions, metrics demoted to reports, explicit Goodhart argument, and dependency direction as a statement about release autonomy and ownership.

### Where the metric is weak **1. Granularity determines the numbers.** I is computed over *external* edges, so merging two components erases edges and splitting one creates them. Two teams with identical code and different packaging conventions get wildly different I values. Consequence: values are comparable only within one consistent boundary definition, and only over time if that definition holds. **2. Every edge weighs one.** A single incidental utility import counts as much as a coupling that threads through fifty call sites and shares types. Real fragility is not uniform across edges, but I cannot see it. A weighted variant (by number of referenced symbols, or by how many of your types appear in their signatures) is more informative but nonstandard. **3. Volatility is invisible.** I is structural. A stable component that never changes costs nothing; a stable component under constant churn is a systemic hazard. Both can have I = 0.05. Any serious audit overlays version-control churn. **4. Cycles break it.** Under a cycle, I(X) ≥ I(Y) and I(Y) ≥ I(X) can only both hold if the values are equal, and the whole notion of "direction of stability" collapses. The Acyclic Dependencies Principle must be satisfied first; treat cycle detection as the higher-priority check. **5. It sees only static, compile-time coupling.** It misses: HTTP/RPC calls resolved by name at runtime, shared database tables or schemas, shared message topics and payload formats, shared configuration keys, and reflection/service-loader wiring. In a distributed system, the import graph can look immaculate while the runtime graph is a knot. Independent deployability — can Y ship without X shipping? — is often the more honest question than any coupling count. **6. Third-party edges skew everything.** Include the standard library and mature frameworks in Ce and every component looks unstable; exclude them and you may hide a genuinely risky dependency on an immature library. Choose a policy and state it. **7. Abstractness (A) is language-dependent.** In duck-typed or structurally-typed languages there may be no declared abstract type to count, making A — and therefore distance-from-main-sequence D — close to meaningless without a convention. ### What to enforce instead: explicit, checkable direction The robust mechanism is a *declared* dependency contract, machine-checked in CI, with violations failing the build: - **Module systems and visibility rules** — Bazel `visibility`, JPMS `requires`/`exports`, Go internal packages, Rust crate boundaries. - **Framework-level allow-lists** — Spring Modulith `@ApplicationModule(allowedDependencies = ...)` verified by a modulith test; Nx project tags with `depConstraints`; dependency-cruiser rules in the JS/TS world. - **Architecture fitness functions** — ArchUnit / dependency-cruiser / import-linter tests asserting layer direction, forbidden edges, and acyclicity, run in the fast CI gate. Why this beats a metric threshold: the rule states *intent* ("payments may not depend on reporting"), fails at the moment of violation with a precise message, and every exception becomes a reviewed, diffable line rather than a slow drift in an aggregate number. ### Where SDP still earns its keep at scale - **Boundary design.** When drawing new component/service seams, place the things you forecast will churn where nothing depends on them, and put the shared contract in a small abstract component with Ce = 0. - **Periodic direction report.** Generate the list of edges where I increases along the arrow. This is a short, human-reviewable agenda, not a gate. - **Churn × I overlay.** Rank components by (low I) × (high change frequency) to find the true hot spots; that ranking survives most of the metric's weaknesses because it is used comparatively within one repo. - **Conway alignment.** Dependency direction encodes who can release independently. If team A's stable component depends on team B's volatile one, team A's release cadence is hostage to team B. Reviewing SDP violations is often really reviewing organizational coupling — and the fix (invert the dependency, give the contract to the stable side) is as much an ownership decision as a code one. ### The Goodhart problem "When a measure becomes a target, it ceases to be a good measure." Publishing I or D per team produces predictable pathologies: interfaces added purely to raise abstractness (manufacturing the Zone of Uselessness), components split or merged to move counts, and vanity "api" modules with no consumers. Keep these numbers in a report a human reads while looking at a specific component, alongside its change history — and keep the *binding* constraint expressed as explicit allowed-dependency rules that fail loudly and are argued about in review.

  • Two services communicate only over HTTP, so the static dependency graph shows no edge between them. What does that mean for SDP?
    The coupling is real and SDP still applies to it — the caller depends on the callee's contract, availability and release cadence — but the import graph cannot see it. You have to model runtime edges explicitly (from API clients, schema registries, service catalogs, topic ownership) or ask the operational question directly: can either side ship a breaking change without coordinating? Shared databases and shared message payloads are the same blind spot.
  • Would you fail a build when a component's I exceeds a threshold?
    No. High I is not a defect — leaf components should have I near 1 — and thresholds invite gaming. Fail the build on *directional* facts instead: a forbidden edge, a new cycle, a dependency not in the declared allow-list. Those are unambiguous, locally explainable, and each exception is a reviewed diff.
  • How does SDP intersect with team topology?
    Dependency direction determines release autonomy: if your stable component depends on another team's volatile one, you inherit their cadence and their breakages. Recurring SDP violations across a team boundary usually signal misplaced ownership — the standard fix is to give the contract to the stable/consuming side and let the volatile side implement it, which also moves the coordination cost to where the churn originates.

saying these in an interview costs you the question

  • Proposing a hard CI threshold on I or D per component
  • Comparing I values across different repos or tools as if they were absolute
  • Reading SDP metrics in a codebase that still has dependency cycles
  • Assuming a clean static import graph means components are decoupled, while they share a database or message schema
  • Treating SDP as purely technical, ignoring the release-autonomy and ownership implications
  • Adding interfaces to improve abstractness scores

context