Skip to main content
Clean Architecture 70 XP · 7 min

SAP: The Stable Abstractions Principle

A component should be as abstract as it is stable — the most stable components should be pure interfaces so they can be extended without modification.

Showing
Ad (728×90)

Why this matters

The Stable Abstractions Principle (SAP) states that a component should be as abstract as it is stable. Combined with the SDP, SAP delivers the Dependency Inversion Principle at the component scale. The formula: Abstraction A = abstract classes / total classes in component. A component with A≈1 is entirely abstract (all interfaces). A component with A≈0 is entirely concrete.

Stable components (I≈0) should have A≈1. If a component is highly depended-upon and cannot change, it must be extendable without modification. The Open/Closed Principle applies at the component level: pure interfaces can be extended by adding new implementations, never by modifying the interface itself. This is how high-level business policies remain stable while behavior is extended.

Volatile components (I≈1) can be concrete (A≈0). If a component has few dependents, it is free to change its concrete implementation. Low-level details like database drivers, email senders, and HTTP adapters should live here — volatile and concrete, easy to swap.

The problem

A highly stable PaymentProcessor component imported by 20 modules contains only concrete classes. Adding ApplePayPayment requires modifying this stable component, rippling changes across all 20 dependents.

Bad

# payment_processor/__init__.py  ← imported by 20 modules (stable, I≈0)
class StripePayment:
    def charge(self, amount: float, token: str) -> bool:
        return True   # Stripe-specific implementation

class PayPalPayment:
    def charge(self, amount: float, token: str) -> bool:
        return True   # PayPal-specific implementation

class CryptoPayment:
    def charge(self, amount: float, token: str) -> bool:
        return True   # Crypto-specific implementation

# A≈0 (all concrete), I≈0 (many depend on it) → Pain Zone
# To add ApplePayPayment: edit this widely-depended-upon component.
# All 20 dependents must be re-tested. OCP violated at component scale.
// payment-processor/index.ts  ← imported by 20 modules (stable, I≈0)
export class StripePayment {
  charge(amount: number, token: string): boolean { return true; }
}

export class PayPalPayment {
  charge(amount: number, token: string): boolean { return true; }
}

export class CryptoPayment {
  charge(amount: number, token: string): boolean { return true; }
}

// A≈0 (all concrete), I≈0 (many depend on it) → Pain Zone
// To add ApplePayPayment: edit this widely-depended-upon component.
// All 20 dependents must be re-tested. OCP violated at component scale.

The solution

The stable payment-processor component exports only the PaymentGateway abstract interface and a data type. Concrete implementations live in a volatile payment-adapters component. Adding Apple Pay touches only the volatile side.

Good

# payment_processor/__init__.py  ← stable (I≈0), abstract (A≈1)
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class PaymentResult:
    success: bool
    transaction_id: str

class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: float, token: str) -> PaymentResult: ...

# payment_adapters/stripe.py  ← volatile (I≈1), concrete (A≈0)
class StripeGateway(PaymentGateway):
    def charge(self, amount: float, token: str) -> PaymentResult:
        return PaymentResult(success=True, transaction_id="stripe_txn_123")

# payment_adapters/applepay.py  ← new: touches only the volatile component
class ApplePayGateway(PaymentGateway):
    def charge(self, amount: float, token: str) -> PaymentResult:
        return PaymentResult(success=True, transaction_id="apple_txn_456")

# 20 dependents import PaymentGateway (abstract) — completely untouched.
# A(payment_processor) ≈ 1, I ≈ 0  → on the Main Sequence.
// payment-processor/index.ts  ← stable (I≈0), abstract (A≈1)
export interface PaymentResult { success: boolean; transactionId: string; }

export interface PaymentGateway {
  charge(amount: number, token: string): Promise;
}

// payment-adapters/StripeGateway.ts  ← volatile (I≈1), concrete (A≈0)
import { PaymentGateway, PaymentResult } from "../payment-processor";

export class StripeGateway implements PaymentGateway {
  async charge(amount: number, token: string): Promise {
    return { success: true, transactionId: "stripe_txn_123" };
  }
}

// payment-adapters/ApplePayGateway.ts  ← new: only touches volatile component
import { PaymentGateway, PaymentResult } from "../payment-processor";

export class ApplePayGateway implements PaymentGateway {
  async charge(amount: number, token: string): Promise {
    return { success: true, transactionId: "apple_txn_456" };
  }
}

// 20 dependents import PaymentGateway (interface) — completely untouched.
// A(payment-processor) ≈ 1, I ≈ 0  → on the Main Sequence.

Key takeaway

The most stable components in your system should contain nothing but interfaces and abstract classes. Concrete code is inherently volatile — it changes when implementation details change. Put your abstractions in the stable center, your implementations at the volatile edge, and let the Dependency Inversion Principle flow naturally across the whole component graph.

Done with this lesson?

Mark it complete to earn XP and track your progress.