Stable Dependencies Principle
Volatile depends on stable, never the reverse.
Dependencies have a direction. Volatile code depends on stable code; never the reverse. Reverse the arrow and the volatile core destabilises everything that imports it.
Opinion
Most architecture diagrams I've been handed in my career are lies because they redraw the import graph the team wishes it had, not the one the build is actually compiling. The Stable Dependencies Principle is the rule that puts the diagram and the graph in the same direction. When a heavily-imported file imports a barely-imported one, that barely-imported file's next change ripples through the graph in ways the team can't see until CI goes red.1Clean Architecture (Pearson, 2017), Ch. 14 “Component Coupling”. The single load-bearing chapter for SDP, SAP, and ADP together — the Component Coupling triad.
The reason it gets confused with A11 Dependency Inversion is that they live in adjacent rooms. DIP is a class-level rule about what kind of thing the code depends on: an abstraction, not a concrete. SDP is a component-level rule about which direction the import points: from less-stable to more-stable. They compose: SDP plus the Stable Abstractions Principle gives Martin's “stable abstractions at every scale” argument, which is DIP scaled to packages.2“DIP in the Wild” (martinfowler.com, 2014). Fowler's framing of what DIP is and is not — useful for showing that SDP and DIP are siblings (different scale, different object) rather than the same rule under different names. Different scales, different objects, same family.
The lever that makes SDP visible is the import graph. Without it the rule is a vibe; with it it is a build failure. A2 Three-Tier Hoisting is already SDP under another name: the generic/ → product/ → module gradient is a stability gradient by construction, and the upward-only import rule is exactly “volatile depends on stable” expressed in file paths.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Volatile depends on stable, never the reverse. Read every import as an arrow — arrows point from less-stable code (high I = Ce / (Ca + Ce)) toward more-stable code (low I). When a stable file grows an import on a volatile one, invert the dependency through an interface or move the volatile thing. /tenet/stable-dependencies-principle/A3
AI eyes only
Rule: volatile depends on stable. Imports flow toward stability. Never the reverse.
Reject: stable libraries importing from volatile features. Reject: utility files importing domain types. Reject: lib/generic/ importing from lib/product/.
Generate: when a stable file needs a volatile detail, invert via interface. The volatile side implements the interface; the stable side depends on the abstraction.
Diagnostic: list the imports of the file you are editing. If any target changes more often than this file, invert the dependency before you ship.
Why?
- The import graph stops being a vibe. With SDP enforced, the direction of every edge is a decision the team can read in the build, not an emergent property of who edited which file last.
- Stability becomes countable: I = Ce / (Ca + Ce). Reviewers stop arguing about whether a file “feels stable” and start asking the metric. The conversation shrinks to the data.
- Caps the blast radius of a change to a stable file. When a low-I file changes, the team knows by construction which high-I leaves will recompile and which won't. Surprises in CI become almost rare.
- Pairs cleanly with A11 Dependency Inversion: SDP picks the direction; DIP picks the shape (abstraction over concrete). Together they give Martin's “stable abstractions at every scale” argument its full form.
- Falls out of A2 Three-Tier Hoisting for free. The
generic/→product/→ module gradient is a stability gradient by construction; A2's upward-only import rule is SDP in folder form. - Surfaces the deprecation conversation early. A file whose I climbs over time is one whose importers are moving away from it — a measurable signal that a v2 is overdue, without waiting for the team to notice in retro.
- Coding agents reach for sideways imports and inheritance because the training data is full of both. The upward-only direction rule forces them through composition or abstraction — the same lever that protects human reviewers also protects the agent from itself.
Origins
Robert C. Martin published the metrics that anchor SDP in “OO Design Quality Metrics: An Analysis of Dependencies” in 1994.5“OO Design Quality Metrics: An Analysis of Dependencies” (1994). The paper that defined Ca, Ce, I, A, and D. The metrical anchor for SDP — the I metric is from this paper, not the 2017 book. Afferent coupling (Ca), efferent coupling (Ce), Instability (I = Ce / (Ca + Ce)), Abstractness (A), and the Distance from the Main Sequence (D) all originate there. The 1994 paper was a quantitative argument: package-level health could be measured the way function-level complexity could be measured, and the measurements predicted where the next regression would land.
Martin formalised SDP in the 1996 C++ Report piece “Granularity” and in the package-design chapters of Agile Software Development: Principles, Patterns, and Practices (2002). The single load-bearing line: “Depend in the direction of stability.”6“Granularity”, C++ Report (1996), and Agile Software Development (2002). The line is single-sentence: depend in the direction of stability. The 1996 piece is where the package-coupling chapter from the 2002 book was first drafted. The chapter pairs SDP with its sibling SAP (Stable Abstractions Principle) so that the most-stable packages are also the most abstract — otherwise the stable parts of the system become the rigid parts.
The book-length restatement is Chapter 14 of Clean Architecture (Pearson, 2017).1Clean Architecture (Pearson, 2017), Ch. 14 “Component Coupling”. The single load-bearing chapter for SDP, SAP, and ADP together — the Component Coupling triad. Martin pairs SDP with the Acyclic Dependencies Principle (ADP) and SAP into a single Component Coupling story: the package graph must be a DAG, the edges must point toward stability, and the most-stable packages must be the most abstract. ADP catches the cycles; SDP catches the bad direction; SAP catches the rigid stable thing.
The instinct is older than the metric. Larry Constantine's coupling hierarchy (1968, published 1974 with Stevens and Myers in “Structured Design”) named direction-of- coupling decades before Martin gave the package version a number.7“Structured Design”, IBM Systems Journal 13(2), 1974. The coupling/cohesion taxonomy that predates SDP by two decades; direction-of-coupling already named, just without the metric. Meilir Page-Jones's connascence taxonomy (1992, 1996) gave the same instinct a more granular vocabulary: connascence of name is weak and local, connascence of identity is strong and dynamic, and the rule is to push every coupling toward the weaker, more local end — which, at the package level, is exactly what SDP's direction rule produces.8What Every Programmer Should Know About Object-Oriented Design (Dorset House, 1995/1996), and the 1992 CACM paper. The connascence taxonomy gives the package-direction instinct a more granular vocabulary; SDP is one rule inside the larger coupling story.
Quotes
Depend in the direction of stability.
I = Ce / (Ca + Ce). This metric has the range [0, 1]. I = 0 indicates a maximally stable category. I = 1 indicates a maximally unstable category.
With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviours of your system will be depended on by somebody.
Dependencies are the primary source of complexity, because they make it hard to modify the system: a change in one place may force changes in other places.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01The paper that defined Ca, Ce, I, A, and D. Argued package-level health is countable, not subjective. The metrical anchor for SDP and SAP both.
- 02The book-length restatement. Pairs SDP with the Acyclic Dependencies Principle and the Stable Abstractions Principle into a single Component Coupling story; argues the package graph must be a DAG with edges pointing toward stability.
- 03“Principles of OOD”SupportsMartin's online compendium of the SOLID + package-design principles. The single page where SDP, SAP and ADP are listed alongside SOLID — useful for showing they belong in the same family.
- 04The community wiki entry that distils SDP into one paragraph and links to its sibling rules. Useful as a beginner-readable pointer; the discussion thread captures the SDP-vs-DIP confusion in real time.
- 05“Principles of Package Coupling”SupportsCohort summary of SDP + SAP + ADP for the Agile-in-a-Flash card series. The single-card framing forces the principle to fit on an index card; useful for the elevator-pitch version.
Sixteen sources, three stances. The supporters cluster on Martin's component-coupling canon: the 1994 metrics paper, the 2017 Clean Architecture chapter, plus the wiki and Agile-in-a-Flash distillations. The qualifiers further down carry the harder reading: stability is one coupling axis among several, and the metric only earns its keep when the team agrees what “stable” means in their context. The opposers carry the steelman the reply has to address: any sufficiently-used interface becomes its own promise, regardless of which way the arrows point.
Examples
// Before: lib/generic/ imports from lib/product/. The arrow points the wrong way.import { Tenet } from "@/lib/product/tenet/tenet.types";export function toSlug(tenet: Tenet): string { return tenet.title.toLowerCase().replace(/\s+/g, "-");}// any Tenet field rename now ripples through every to-slug caller.
// After: generic takes a primitive. The Tenet-aware caller composes the two.// lib/generic/string/to-slug.ts (no upward import):export function toSlug(value: string): string { return value.toLowerCase().replace(/\s+/g, "-");}// lib/product/tenet/get-tenet-slug.ts composes the two:import { toSlug } from "@/lib/generic/string/to-slug";export const getTenetSlug = (tenet: Tenet): string => toSlug(tenet.title);
// Before: lib/generic/ imports from lib/product/. The arrow points the wrong way.import { Tenet } from "@/lib/product/tenet/tenet.types";export function toSlug(tenet: Tenet): string { return tenet.title.toLowerCase().replace(/\s+/g, "-");}// any Tenet field rename now ripples through every to-slug caller.
// After: generic takes a primitive. The Tenet-aware caller composes the two.// lib/generic/string/to-slug.ts (no upward import):export function toSlug(value: string): string { return value.toLowerCase().replace(/\s+/g, "-");}// lib/product/tenet/get-tenet-slug.ts composes the two:import { toSlug } from "@/lib/generic/string/to-slug";export const getTenetSlug = (tenet: Tenet): string => toSlug(tenet.title);
// Before: lib/generic/ imports from lib/product/. The arrow points the wrong way.import { TENET_KEYS } from "@/lib/product/tenet/tenet-keys";export function toSlug(tenet) { return tenet.title.toLowerCase().replace(/\s+/g, "-");}// any Tenet field rename now ripples through every to-slug caller.
// After: generic takes a primitive. The Tenet-aware caller composes the two.// lib/generic/string/to-slug.js (no upward import):export function toSlug(value) { return value.toLowerCase().replace(/\s+/g, "-");}// lib/product/tenet/get-tenet-slug.js composes the two:import { toSlug } from "@/lib/generic/string/to-slug";export const getTenetSlug = (tenet) => toSlug(tenet.title);
# Before: prickles.generic imports from prickles.product. The arrow points the wrong way.from prickles.product.tenet.tenet_types import Tenetdef to_slug(tenet: Tenet) -> str: return re.sub(r"\s+", "-", tenet.title.lower())# any Tenet field rename now ripples through every to_slug caller.
# After: generic takes a primitive. The Tenet-aware caller composes the two.# prickles/generic/string/to_slug.py (no upward import):def to_slug(value: str) -> str: return re.sub(r"\s+", "-", value.lower())# prickles/product/tenet/get_tenet_slug.py composes the two:from prickles.generic.string.to_slug import to_slugdef get_tenet_slug(tenet: Tenet) -> str: return to_slug(tenet.title)
// Before: com.prickles.generic imports from com.prickles.product. The arrow points the wrong way.package com.prickles.generic.string;import com.prickles.product.tenet.Tenet;public class ToSlug { public static String run(Tenet tenet) { return tenet.title().toLowerCase().replaceAll(String.raw`\s+`, "-"); }}// any Tenet field rename now ripples through every ToSlug caller.
// After: generic takes a primitive. The Tenet-aware caller composes the two.// com/prickles/generic/string/ToSlug.java (no upward import):public class ToSlug { public static String run(String value) { return value.toLowerCase().replaceAll(String.raw`\s+`, "-"); }}// com/prickles/product/tenet/GetTenetSlug.java composes the two:import com.prickles.generic.string.ToSlug;
// Before: Prickles\Generic imports from Prickles\Product. The arrow points the wrong way.namespace Prickles\Generic\StringHelpers;use Prickles\Product\Tenet\Tenet;class ToSlug { public static function run(Tenet $tenet): string { return preg_replace('/\s+/', '-', strtolower($tenet->title)); }}// any Tenet field rename now ripples through every ToSlug caller.
// After: generic takes a primitive. The Tenet-aware caller composes the two.// src/Generic/StringHelpers/ToSlug.php (no upward import):class ToSlug { public static function run(string $value): string { return preg_replace('/\s+/', '-', strtolower($value)); }}// src/Product/Tenet/GetTenetSlug.php composes the two:use Prickles\Generic\StringHelpers\ToSlug;
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| import/no-restricted-paths | eslint-plugin-import | any import edge that points from a stable tier to a volatile tier — a generic file growing an import on a product file, or a product file growing an import on an app route. |
| import/no-cycle | eslint-plugin-import | import cycles between any two packages — the symptom that two files are at the same stability tier and have grown into each other rather than past each other. |
| validate / metrics output | dependency-cruiser | the same upward-only contract as a standalone CI step, plus per-package Ca/Ce/I/A/D metrics — the numbers SDP is built on, machine-readable. |
| import/no-relative-packages | eslint-plugin-import | relative imports across package boundaries — the import shape that quietly defeats SDP by routing around the public surface. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { import: importPlugin },
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{
target: './src/lib/generic',
from: ['./src/lib/product', './src/components', './src/app'],
message: 'volatile depends on stable, never the reverse — generic is the most stable tier.',
},
{
target: './src/lib/product',
from: ['./src/components', './src/app'],
message: 'product is more stable than the leaves; leaves depend on product, not the reverse.',
},
],
}],
'import/no-cycle': ['error', { maxDepth: 10 }],
'import/no-relative-packages': 'error',
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| import/no-restricted-paths | eslint-plugin-import | any import edge that points from a stable tier to a volatile tier — a generic file growing an import on a product file, or a product file growing an import on an app route. |
| import/no-cycle | eslint-plugin-import | import cycles between any two packages — the symptom that two files are at the same stability tier and have grown into each other rather than past each other. |
| validate / metrics output | dependency-cruiser | the same upward-only contract as a standalone CI step, plus per-package Ca/Ce/I/A/D metrics — the numbers SDP is built on, machine-readable. |
| import/no-relative-packages | eslint-plugin-import | relative imports across package boundaries — the import shape that quietly defeats SDP by routing around the public surface. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { import: importPlugin },
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{
target: './src/lib/generic',
from: ['./src/lib/product', './src/components', './src/app'],
message: 'volatile depends on stable, never the reverse — generic is the most stable tier.',
},
{
target: './src/lib/product',
from: ['./src/components', './src/app'],
message: 'product is more stable than the leaves; leaves depend on product, not the reverse.',
},
],
}],
'import/no-cycle': ['error', { maxDepth: 10 }],
'import/no-relative-packages': 'error',
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| import/no-restricted-paths | eslint-plugin-import | stability-gradient violations between packages. |
| import/no-cycle | eslint-plugin-import | cycles between any two packages. |
| metrics + forbidden | dependency-cruiser | the same upward-only contract plus the per-package I metric — SDP's direction rule and SDP's metric in a single CI step. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { import: importPlugin },
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{
target: './src/lib/generic',
from: ['./src/lib/product', './src/components', './src/app'],
message: 'volatile depends on stable, never the reverse — generic is the most stable tier.',
},
{
target: './src/lib/product',
from: ['./src/components', './src/app'],
message: 'product is more stable than the leaves; leaves depend on product, not the reverse.',
},
],
}],
'import/no-cycle': ['error', { maxDepth: 10 }],
'import/no-relative-packages': 'error',
}
});Apply these rules in .importlinter. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| layers contract | import-linter | the upward-only stack from generic at the bottom through product to features at the top — SDP's direction rule expressed as a contract. |
| forbidden contract | import-linter | any specific reverse edge — useful for one-off carve-outs the layers contract can't express on its own. |
| independence contract | import-linter | two packages at the same stability tier that should not depend on each other — sibling subdomains accidentally importing across the boundary. |
| grimp graph queries | grimp | ad-hoc questions about the import graph — what depends on what, where the cycles are, the per-package counts SDP's I metric is computed from. |
.importlinterconfiguration snippet
[importlinter]
root_packages =
myapp
[importlinter:contract:upward-only]
name = Volatile depends on stable, never the reverse
type = layers
layers =
myapp.app
myapp.features
myapp.lib.product
myapp.lib.generic
containers =
myapp
[importlinter:contract:no-cycles]
name = The package graph must be a DAG
type = forbidden
source_modules =
myapp.lib.generic
forbidden_modules =
myapp.lib.product
myapp.features
myapp.appApply these rules in src/test/java/com/example/archtests/StableDependenciesTest.java. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| layeredArchitecture() | ArchUnit | any class that depends on a layer above it in the stability gradient — generic depending on product, product depending on features. |
| slices().beFreeOfCycles() | ArchUnit | import cycles between sibling packages — the symptom that two packages have grown into each other rather than past each other. |
| Ca / Ce / I / A / D metrics | JDepend | package-level coupling and stability metrics — the numbers Martin's 1994 paper defined, computed from the compiled bytecode. |
| freeze rule + violationStore | ArchUnit | lets you adopt SDP on an existing codebase by freezing today's violations and failing the build only on new ones — the only realistic path on a 100k-line legacy tree. |
src/test/java/com/example/archtests/StableDependenciesTest.javaconfiguration snippet
package com.example.archtests;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
@AnalyzeClasses(packages = "com.example")
public class StableDependenciesTest {
@ArchTest
static final ArchRule volatile_depends_on_stable =
layeredArchitecture().consideringAllDependencies()
.layer("Generic").definedBy("com.example.lib.generic..")
.layer("Product").definedBy("com.example.lib.product..")
.layer("Features").definedBy("com.example.features..")
.whereLayer("Features").mayNotBeAccessedByAnyLayer()
.whereLayer("Product").mayOnlyBeAccessedByLayers("Features")
.whereLayer("Generic").mayOnlyBeAccessedByLayers("Product", "Features");
@ArchTest
static final ArchRule package_graph_is_a_dag =
slices().matching("com.example.(*)..").should().beFreeOfCycles();
}Apply these rules in deptrac.yaml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| layers + ruleset | Deptrac | any class in one tier that depends on a tier above it — SDP's direction rule expressed as a directed graph. |
| directory collector | Deptrac | namespaces that drift out of their assigned tier — useful when the file path is the source of truth, not the namespace. |
| should-not-depend-on rules | PHPArkitect | the same upward-only rule expressed in PHP rather than YAML — useful for repos that already lean on fluent assertions. |
| should-not-have-circular-dependencies | Deptrac | circular dependencies between any two packages — the symptom that ADP (SDP's sibling) is being violated. |
deptrac.yamlconfiguration snippet
deptrac:
paths:
- ./src
layers:
- name: Generic
collectors:
- type: directory
regex: src/Lib/Generic/.*
- name: Product
collectors:
- type: directory
regex: src/Lib/Product/.*
- name: Features
collectors:
- type: directory
regex: src/Features/.*
ruleset:
Generic: ~
Product:
- Generic
Features:
- Product
- GenericAI rules
.cursor/rules/a3-stable-dependencies.mdc---
description: Prickles A3 — Stable Dependencies Principle
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles A3 — Stable Dependencies Principle
Volatile depends on stable, never the reverse. Read every import edge as an arrow; arrows must point from less-stable code toward more-stable code.
Stability is countable: I = Ce / (Ca + Ce). High-I files are leaves (depend on lots, depended on by few); low-I files are roots. Imports must point from high-I toward low-I.
Pair SDP with the Stable Abstractions Principle (SAP): the most-stable packages must also be the most abstract. Otherwise the stable parts of the system become the rigid parts.
Refuse to grow a stable file with an import on a volatile one. If you would, invert the dependency through an interface or move the volatile thing into a less-stable tier.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The strongest steelman comes from John Ousterhout combined with Hyrum's Law.3A Philosophy of Software Design (2018, 2nd ed. 2021). The strongest steelman for the “module depth matters more than direction” counter; SDP's reply is that depth lives one level down from this rule and both ship. An interface with enough callers will be relied on for every observable property, not just the documented ones, which means “stable” in practice is a consequence of usage, not a property the rule can dictate. SDP says “volatile depends on stable” as if stability were an attribute of the file; in practice it is an attribute of the social contract around the file, and the metric I = Ce / (Ca + Ce) is a proxy for that contract that misses the unwritten parts. The implication: enforcing SDP on a metric that lags the social reality can produce a folder structure that looks healthy and a team that still pays for every “stable” file's every change.
Counter-argument retort
Hyrum's observation is correct, and SDP doesn't pretend otherwise. The reply is that SDP is the rule for the part of stability the team controls — the import-graph direction — not for the part it doesn't. A file that's currently stable by the metric and unstable by the social contract is a deprecation problem, not an architecture one; the next move is to publish a v2 surface and migrate the callers, exactly the cycle the Stable Abstractions Principle is here to support.4Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002). Ch. 28 introduces SDP's sibling SAP (Stable Abstractions Principle) — the rule that stable packages should also be abstract, so they aren't rigid. SDP catches the bad direction; SAP catches the bad shape; together they cover the part of stability the build can see.
The Ousterhout depth concern is real but lives one level down from this rule. SDP tells you which direction edges should point in the package graph; module depth tells you how those packages should organise their internals.3A Philosophy of Software Design (2018, 2nd ed. 2021). The strongest steelman for the “module depth matters more than direction” counter; SDP's reply is that depth lives one level down from this rule and both ship. Both rules ship; neither replaces the other. A package with the right import direction and a shallow interface is still a leaky abstraction; a package with a deep interface and a wrong import direction is a time bomb. The team needs both metrics, and the tools to read both metrics live in different files.
The genuine residue is the conflation with A11 Dependency Inversion. They're different rules at different scales: DIP is class-level (depend on abstractions, not concretes); SDP is package-level (depend on stable components, not volatile ones). The case file pins the distinction once so readers don't conflate them. SDP + SAP is closer to DIP scaled up, but it's still not the same rule — SDP is about direction, DIP is about shape.2“DIP in the Wild” (martinfowler.com, 2014). Fowler's framing of what DIP is and is not — useful for showing that SDP and DIP are siblings (different scale, different object) rather than the same rule under different names. Both belong in the canon for the same reason F1 Single Responsibility (function-scale) and A4 Common Closure (package-scale) both belong: scale matters, and the same instinct earns separate rules at separate altitudes.
The novelty challenge — that this is just “import graph cleanliness” under a fancy name — misses what the metric does. Without I and Ca/Ce, “cleanliness” is a vibe. With it, “the metric drifted from 0.1 to 0.4 this quarter on the auth package” is a sentence the team can act on. The metric earns the rule.
Notes
- [1]Robert C. Martin — Clean Architecture (Pearson, 2017), Ch. 14 “Component Coupling”. The single load-bearing chapter for SDP, SAP, and ADP together — the Component Coupling triad.
- [2]Martin Fowler — “DIP in the Wild” (martinfowler.com, 2014). Fowler's framing of what DIP is and is not — useful for showing that SDP and DIP are siblings (different scale, different object) rather than the same rule under different names.
- [3]John Ousterhout — A Philosophy of Software Design (2018, 2nd ed. 2021). The strongest steelman for the “module depth matters more than direction” counter; SDP's reply is that depth lives one level down from this rule and both ship.