Why this matters
The Stable Dependencies Principle (SDP) governs which components are allowed to depend on which other components. The rule is simple: depend in the direction of stability. Never let a stable component depend on a volatile one.
Stability is measured, not assumed. A component is stable if it is hard to change. It becomes hard to change when many other components depend on it — because any change would require updating all those dependents. The metric is Instability: I = fan-out / (fan-in + fan-out). Fan-in counts incoming dependencies (components that depend on this one). Fan-out counts outgoing dependencies (components this one depends on). I≈0 means stable; I≈1 means volatile.
The SDP rule: the Instability (I) of a depended-upon component should be greater than or equal to the I of the depending component. Stable components (I≈0) should depend only on other stable components. Volatile components (I≈1) can depend on anything — they are at the leaf of the dependency tree.
When a stable component depends on a volatile one, the stable component's stability is undermined. Every time the volatile component changes, the stable component must also change — and so do all of its dependents. The SDP violation propagates change through the system like a shock wave.
The problem
A stable AuthService (imported by 10 modules) directly imports ExperimentalFeatureFlag — a volatile class that changes weekly. One change to the experiment breaks every module depending on Auth.
Bad
# auth_service.py ← imported by 10 other modules
from experimental.feature_flag import ExperimentalFeatureFlag # volatile!
class AuthService:
def __init__(self):
self._flag = ExperimentalFeatureFlag() # coupling to volatile concrete
def can_access(self, user_id: str, feature: str) -> bool:
if not self._flag.is_enabled(feature): # volatile call inside stable class
return False
return self._is_authorised(user_id)
# experimental/feature_flag.py ← changes every week, nobody depends on it
class ExperimentalFeatureFlag:
def is_enabled(self, feature: str) -> bool:
return feature in {"new_dashboard", "beta_checkout"}
# Instability(AuthService) ≈ 0 (many depend on it — should be stable)
# Instability(FeatureFlag) ≈ 1 (nobody depends on it — volatile)
# SDP violated: stable depends on volatile.
// AuthService.ts ← imported by 10 other modules
import { ExperimentalFeatureFlag } from "./experimental/ExperimentalFeatureFlag";
export class AuthService {
private flag = new ExperimentalFeatureFlag(); // coupling to volatile concrete
canAccess(userId: string, feature: string): boolean {
if (!this.flag.isEnabled(feature)) return false; // volatile call inside stable
return this.isAuthorised(userId);
}
private isAuthorised(userId: string): boolean { return true; }
}
// ExperimentalFeatureFlag.ts ← changes every week, nobody depends on it
export class ExperimentalFeatureFlag {
isEnabled(feature: string): boolean {
return ["new_dashboard", "beta_checkout"].includes(feature);
}
}
// Instability(AuthService) ≈ 0 (many depend on it — should be stable)
// Instability(ExperimentalFlag) ≈ 1 (nobody depends on it — volatile)
// SDP violated: stable depends on volatile.
The solution
AuthService depends on a FeatureFlagPort abstract interface (stable). ExperimentalFeatureFlag implements it and stays at the volatile edge. Auth's stability is preserved.
Good
# ports/feature_flag_port.py ← stable abstract interface
from abc import ABC, abstractmethod
class FeatureFlagPort(ABC):
@abstractmethod
def is_enabled(self, feature: str) -> bool: ...
# auth_service.py ← depends only on the abstract port (stable)
class AuthService:
def __init__(self, flags: FeatureFlagPort):
self._flags = flags
def can_access(self, user_id: str, feature: str) -> bool:
if not self._flags.is_enabled(feature):
return False
return self._is_authorised(user_id)
# experimental/feature_flag.py ← volatile concrete at the unstable edge
class ExperimentalFeatureFlag(FeatureFlagPort):
def is_enabled(self, feature: str) -> bool:
return feature in {"new_dashboard", "beta_checkout"}
# Instability(FeatureFlagPort) ≈ 0 — abstract, depended upon
# Instability(ExperimentalFeatureFlag) ≈ 1 — concrete, at the edge
# SDP satisfied: dependencies flow toward stability.
// ports/FeatureFlagPort.ts ← stable abstract interface
export interface FeatureFlagPort {
isEnabled(feature: string): boolean;
}
// AuthService.ts ← depends only on the stable interface
import { FeatureFlagPort } from "./ports/FeatureFlagPort";
export class AuthService {
constructor(private readonly flags: FeatureFlagPort) {}
canAccess(userId: string, feature: string): boolean {
if (!this.flags.isEnabled(feature)) return false;
return this.isAuthorised(userId);
}
private isAuthorised(userId: string): boolean { return true; }
}
// experimental/ExperimentalFeatureFlag.ts ← volatile concrete at the edge
import { FeatureFlagPort } from "../ports/FeatureFlagPort";
export class ExperimentalFeatureFlag implements FeatureFlagPort {
isEnabled(feature: string): boolean {
return ["new_dashboard", "beta_checkout"].includes(feature);
}
}
// Instability(FeatureFlagPort) ≈ 0 — abstract, depended upon
// Instability(ExperimentalFeatureFlag) ≈ 1 — concrete, at the edge
// SDP satisfied: dependencies flow toward stability.
Key takeaway
Stability is not about frequency of change — it is about how many components depend on you. A stable component (I≈0) is hard to change because change propagates to all dependents. Never let a stable component import a volatile one; insert an abstract interface at the boundary so that the dependency arrow points toward stability, not away from it.