Stable Dependencies Principle: Point Toward Stability
A dependency graph is not only a map of which component calls which other component. Its direction also determines which parts of a system can change independently.
Consider a reporting component used by ten other components. Many callers depend on it, so changing its public contract can require coordinated work across the codebase. That reporting component has become relatively stable: not necessarily because its code rarely changes, but because many other components constrain how freely its contract can change.
Now suppose that component depends directly on a small experimental formatting package that changes every week. A component with many obligations is tied to one with few obligations. The dependency direction works against the natural pressure of the system.
The Stable Dependencies Principle gives a useful rule for component architecture:
Dependencies should point in the direction of stability.
This is a statement about dependency structure, not a command to freeze code.
Stability is about responsibility to dependents
In everyday language, stable often means unchanged. In component design, a more useful meaning is hard to move without affecting other things.
A table with one heavy leg fixed to the floor is stable because moving it requires effort. A component can be stable for a similar structural reason: many other components depend on it.
Suppose the dependency graph is:
checkout ----\
billing ------> pricing
reports -----/pricing has three incoming dependencies. Its contract carries obligations to three consumers.
Now compare a component that only depends on others:
database
^
|
importer
|
v
parserIf nothing depends on importer, its maintainers can often replace or reorganize it with limited impact outside its boundary. It is relatively easy to move.
Stability therefore describes dependency pressure, not code age, release frequency, or developer preference.
A simple instability metric
One common way to make the idea concrete is to count incoming and outgoing component dependencies.
Let:
Cabe afferent coupling: components that depend on this component.Cebe efferent coupling: components this component depends on.
Then define instability as:
I = Ce / (Ca + Ce)The result ranges from 0 to 1.
A component with five incoming dependencies and no outgoing dependencies has:
Ca = 5
Ce = 0
I = 0 / (5 + 0) = 0It is maximally stable by this metric.
A component with no incoming dependencies and five outgoing dependencies has:
Ca = 0
Ce = 5
I = 5 / (0 + 5) = 1It is maximally unstable.
The word unstable is not an insult here. A component near 1 can be exactly where volatile application-specific code belongs. It has few dependents, so it can change with less coordination.
The metric is a diagnostic aid rather than a quality score.
Dependency direction creates architectural pressure
Imagine four components:
web
|
v
application
|
v
domain
|
v
vendor-sdkAssume many parts of the product depend on domain, directly or indirectly. The vendor SDK changes frequently and exposes provider-specific types.
If domain imports that SDK, stable business concepts become coupled to a volatile external detail. An SDK upgrade can force edits in a component that many other parts of the system rely on.
The problem is not that external libraries are bad. The problem is the direction of source-code dependency.
A boundary can reverse it:
web
|
v
application
|
v
domain <----- adapter
|
v
vendor-sdkThe domain defines the capability it needs. The adapter implements that contract and translates provider-specific details.
Now the volatile adapter depends on the more stable policy-facing boundary. A provider migration can often remain inside the adapter and assembly code.
This is dependency inversion serving component stability.
Stable does not mean concrete or low-level
A common mistake is to treat infrastructure as inherently stable because databases, queues, and frameworks sit underneath application code at runtime.
Runtime call direction and source-code dependency direction are separate concerns.
An application can call a database adapter at runtime even when the adapter depends on an application-owned interface at compile time:
source dependencies:
application <--- postgres-adapter
runtime interaction:
application ---> postgres-adapter ---> databaseThe first diagram matters for the Stable Dependencies Principle. It shows which code must know another component’s definitions.
Stable policy should not need to import volatile mechanism merely because the mechanism executes below it.
Stability should match the cost of change
Suppose a shared money component is imported by twenty components. Its types appear throughout public APIs.
That component has high incoming coupling. Changing its representation may require broad coordination. This is not automatically a design flaw. Money semantics may be central enough to justify that stability.
Now suppose the same shared component also imports:
- one payment provider SDK,
- a UI formatting toolkit,
- an analytics client,
- an experimental feature-flag package.
Its broad responsibility to dependents is now combined with several volatile dependencies. Changes from unrelated mechanisms can propagate into a highly depended-on component.
A better structure separates stable concepts from replaceable details:
+--> payment-adapter --> provider-sdk
|
orders --> money <-+
|
+--> ui-adapter ------> formatting-toolkitThe exact graph will vary, but the pressure should be visible: code with many dependents should avoid unnecessary dependence on code that is easier and more likely to move.
Use the metric at component scale
Counting every class or function can create noise. The principle is most useful at boundaries that have architectural meaning:
- packages,
- modules,
- libraries,
- services within a monorepo,
- deployable components,
- bounded subsystems.
Choose a level where dependency direction can actually guide ownership and change.
For example, suppose a repository contains:
packages/
catalog-domain/
catalog-api/
search-adapter/
postgres-adapter/
telemetry/A dependency report might show:
component Ca Ce I
catalog-domain 4 1 0.20
catalog-api 1 3 0.75
search-adapter 0 2 1.00
postgres-adapter 0 2 1.00
telemetry 5 0 0.00These numbers do not declare that telemetry is excellent or that search-adapter is poor. They describe structural positions.
The useful question is whether dependency edges generally point from higher-instability components toward lower-instability components.
Spot edges that run against the gradient
Consider:
A ----> B
I(A) = 0.25
I(B) = 0.90A is more stable than B, yet A depends on B. This edge runs against the desired stability gradient.
That is a prompt for inspection, not an automatic refactoring order.
Ask what the dependency represents:
- Is
Ban implementation detail that can sit behind an interface? - Is the boundary drawn at the wrong place?
- Are provider-specific types leaking into a stable API?
- Does
Aactually need the whole dependency? - Would moving a small policy abstraction into the stable side reverse the source dependency?
- Is the apparent violation harmless because both components are released and changed as one unit?
The graph points to a conversation. Context determines the change.
Do not chase perfect numbers
A dependency graph with mathematically tidy values can still be poorly designed.
Coupling counts treat every edge equally. In practice, one dependency on a huge unstable framework surface may create more change pressure than three dependencies on tiny, mature contracts.
The metric also ignores semantic importance. A component with I = 0 can expose a terrible API. A component with I = 1 can contain essential, well-designed code.
Use instability to reveal structural pressure, then inspect the actual contracts.
The goal is not:
make every component stableThat would be impossible. If every component had no outgoing dependencies, nothing could collaborate.
The goal is closer to:
put volatile choices near the edges
|
v
depend on progressively more stable policyA healthy architecture needs unstable components. They are useful places for composition, delivery mechanisms, adapters, and product-specific choices.
Stability creates a responsibility to abstract
There is an important consequence to becoming highly stable.
If many components depend on a component, that component is difficult to change freely. It should therefore avoid forcing consumers to depend on volatile implementation details.
For example, this stable interface exposes a provider type:
import { VendorPayment } from "vendor-payments";
export interface BillingService {
charge(payment: VendorPayment): Promise<void>;
}Every consumer now participates in the provider’s model.
A policy-facing contract can instead use application concepts:
export type ChargeRequest = {
orderId: string;
amountInCents: number;
currency: string;
};
export interface BillingService {
charge(request: ChargeRequest): Promise<void>;
}An adapter translates ChargeRequest into the provider representation.
This does not guarantee a good abstraction. It does remove one source of volatility from the stable boundary.
Refactor dependency direction incrementally
Reversing a dependency does not require an architecture rewrite.
Suppose orders directly imports a shipping SDK:
orders ---> shipping-sdkStart by identifying the narrow capability the order workflow actually needs:
export interface ShipmentBooking {
book(orderId: string, destination: Address): Promise<BookingId>;
}Place that contract with the policy that consumes it, or in a boundary owned by that policy.
Then implement it in an adapter:
export class VendorShipmentBooking implements ShipmentBooking {
constructor(private readonly client: ShippingClient) {}
async book(
orderId: string,
destination: Address
): Promise<BookingId> {
const result = await this.client.createShipment({
reference: orderId,
postalCode: destination.postalCode,
country: destination.countryCode,
});
return result.id;
}
}The resulting source dependencies become:
orders <--- shipping-adapter ---> shipping-sdkFinally, assemble the implementation at the application boundary.
This change can be done one integration at a time. The useful unit is a dependency edge whose direction creates real change pressure.
Check the actual release boundary
Component stability depends on what can change independently.
If two packages live in separate directories but are always versioned, tested, and deployed together, treating every import between them as an architectural boundary may exaggerate the significance of those edges.
Conversely, two modules in one repository may have distinct ownership and many consumers. Their dependency direction can matter greatly even without separate deployment.
Before acting on coupling numbers, identify the boundary that carries a real coordination cost:
source module
package
library
team-owned component
deployable service
public APIMeasure at the level that matches the decision.
A practical review sequence
When examining a component graph, use this sequence:
1. Identify components with many incoming dependencies.
2. Inspect their outgoing dependencies.
3. Mark dependencies that are volatile or implementation-specific.
4. Check whether those edges point from stable policy to unstable detail.
5. Introduce a narrower boundary only where it reduces real change pressure.
6. Recalculate the graph after the structural change.This keeps the principle connected to concrete maintenance costs.
A useful architecture review should be able to name the expected benefit: fewer consumers affected by a provider upgrade, less framework vocabulary in domain APIs, a smaller migration surface, or a component that can now change independently.
Keep the principle directional
The Stable Dependencies Principle is easiest to apply when treated as a rule about direction.
It does not say:
- stable components must never change;
- unstable components are defective;
- every dependency needs an interface;
- infrastructure must always depend on domain code;
- coupling counts can replace design judgment.
It says that dependency structure should generally move from components that are easier to change toward components that are harder to change.
That direction aligns source-code structure with the coordination pressure already present in the system.
When the graph points the other way, a volatile detail can pull a stable component into unnecessary change. When the graph follows the stability gradient, volatile choices remain easier to replace and heavily depended-on policy can keep a smaller, steadier surface.