Skip to main content
Clean Architecture 70 XP · 7 min

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.

Showing
Ad (728×90)

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.

Done with this lesson?

Mark it complete to earn XP and track your progress.