SDP: The Stable Dependencies Principle
Depend in the direction of stability — a volatile component should never be depended upon by a stable one, or that stability is permanently compromised.
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.
🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.
Hint: Stability is not about frequency of change — it's about how hard it is to change. A component with many dependents is hard to change, whether or not it changes often.
✗ Your version