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 rather than the one the build is 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.1
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 rather than 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 Different scales and different objects in the 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. 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 merge.
Why?
- The import graph is no longer 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 pushes 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 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. The measurements predicted where the next regression would surface.
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 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).1 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 the direction of coupling decades before Martin gave the package version a number.7 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.8
Quotes
Depend in the direction of stability.
This metric has the range [0,1]. I=0 indicates a maximally stable category. I=1 indicates a maximally instable category.
With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.
Complexity is caused by two things: dependencies and obscurity.
Evidence
16 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 SOLID and the 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 format squeezes the principle onto an index card. Useful for the elevator-pitch version.
- 06“Packaging Principles Part 2”SupportsTwo-part walkthrough of Martin's component principles with worked examples. Part 2 covers SDP and SAP specifically. Readable companion to the 2017 book chapter.
- 07Robert C. Martin CollectionSupportsIndex of every Martin principle in one place with cross-links. Useful for showing readers where SDP sits inside the larger Martin canon: between the SOLID class-level rules and the package-cohesion rules (REP, CCP, CRP).
- 08Modern restatement that grounds package coupling in DDD's subdomain types. Qualifies SDP by saying stability is a property of the subdomain (core, supporting, generic), not just of the package.
- 09Argues complexity is the master metric, with dependencies and obscurity as its two causes. The reply: SDP handles the dependency-direction half; module depth handles the interface-shape half. Both rules apply.
- 10Hyrum's LawOpposes“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.” The strongest steelman: stability is a social fact the metric lags.
- 11Wikipedia: Package PrinciplesSupportsIndex of the six Martin package principles (REP, CCP, CRP, ADP, SDP, SAP). Useful one-paragraph summaries plus the canonical reference list. The entry point for readers new to the family.
- 12“Connascence”QualifiesConnascence is the more granular sibling vocabulary. Qualifies SDP by saying direction-of-dependency is one coupling axis among nine (Page-Jones). The rule justifies itself most when paired with locality and degree.
- 13connascence.ioQualifiesPractitioner reference for the nine forms and three properties. Frames package coupling as one part of a larger coupling story. The case file uses connascence as the formal vocabulary alongside SDP's direction rule.
- 14The earlier origin. Parnas argued module decomposition should hide design decisions behind interfaces. Dependencies should point toward the hidden part, not the volatile part. Forty years before SDP gave the rule a number.
- 15“Coupling, Cohesion & Connascence”QualifiesModern primer that places SDP inside the larger coupling story. Useful for readers who arrive thinking “coupling” means one thing; Stemmler shows it splits into a taxonomy and SDP is one rule inside that taxonomy.
- 16“DIP in the Wild”QualifiesThe disambiguation aid. Fowler's DIP write-up frames what DIP is and is not. Useful for the case file's SDP-vs-DIP callout because it shows the two rules as siblings, not synonyms.
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 hold the harder reading: stability is one coupling axis among several. The metric is only useful when the team agrees what “stable” means in their context. The opposers hold 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.tsno upward importexport function toSlug(value: string): string { return value.toLowerCase().replace(/\s+/g, "-"); }
lib/ product/ tenet/ get-tenet-slug.tscomposes the twoimport { 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.tsno upward importexport function toSlug(value: string): string { return value.toLowerCase().replace(/\s+/g, "-"); }
lib/ product/ tenet/ get-tenet-slug.tscomposes the twoimport { 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.jsno upward importexport function toSlug(value) { return value.toLowerCase().replace(/\s+/g, "-"); }
lib/ product/ tenet/ get-tenet-slug.jscomposes the twoimport { 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 Tenet def 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.pyno upward importdef to_slug(value: str) -> str: return re.sub(r"\s+", "-", value.lower())
prickles/ product/ tenet/ get_tenet_slug.pycomposes the twofrom prickles.generic.string.to_slug import to_slug def 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("\\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.javano upward importpublic class ToSlug { public static String run(String value) { return value.toLowerCase().replaceAll("\\s+", "-"); } }
com/ prickles/ product/ tenet/ GetTenetSlug.javacomposes the twoimport 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.phpno upward importclass ToSlug { public static function run(string $value): string { return preg_replace('/\s+/', '-', strtolower($value)); } }
src/ Product/ Tenet/ GetTenetSlug.phpcomposes the twouse 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 defeats SDP by routing around the public surface. |
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default defineConfig({
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 defeats SDP by routing around the public surface. |
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default defineConfig({
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 { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default defineConfig({
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 =
app
features
lib.product
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. |
| ruleset (one direction only) | 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
value: src/Lib/Generic/.*
- name: Product
collectors:
- type: directory
value: src/Lib/Product/.*
- name: Features
collectors:
- type: directory
value: src/Features/.*
ruleset:
Generic: ~
Product:
- Generic
Features:
- Product
- GenericAI rules
.cursor/rules/a3-stable-dependencies.mdc--- alwaysApply: true --- ## Prickles A3: Stable Dependencies Principle Make volatile code depend on stable code, never the reverse. Read every import edge as an arrow: it 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.3 An interface with enough callers will be relied on for every observable property, documented or not, which means “stable” in practice is a consequence of usage rather than 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. 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 what the team controls about stability (the import-graph direction) and not for what it doesn't. A file that's currently stable by the metric and unstable by the social contract is a deprecation problem rather than 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.4 SDP catches the bad direction and 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.3 Both rules apply and 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. 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 Both belong in the canon for the same reason F1 Single Responsibility (function-scale) and A4 Common Closure (package-scale) both belong: scale matters, so 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 account 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 over direction” counter; SDP's reply is that depth lives one level down from this rule and both apply.
- [4]Robert C. Martin: Agile 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.
- [5]Robert C. Martin: “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 rather than the 2017 book.
- [6]Robert C. Martin: “Granularity”, C++ Report (1996), and Agile Software Development (2002). The line is single-sentence: depend in the direction of stability. The package-coupling chapter of the 2002 book was first drafted in the 1996 article.
- [7]Stevens, Myers & Constantine: “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.
- [8]Meilir Page-Jones: What 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.