DEV Community

Yannick Loth
Yannick Loth

Posted on

Inheritance vs Composition: The Principle of Independent Variation Explains Why

Inheritance vs Composition: The Principle of Independent Variation Explains Why

"Favor composition over inheritance" is one of the most cited design guidelines in software engineering. But have you ever wondered why this works—and more importantly, when inheritance is actually appropriate?

The Principle of Independent Variation (PIV) provides a clear, principled answer by focusing on independent change drivers.

The Core Insight

PIV's guideline is elegantly simple:

When two concerns vary independently → prefer composition

When two concerns vary dependently → inheritance may be appropriate

Let me show you what this means in practice.

A Tale of Two Change Drivers

Consider a payment processing system. At first glance, you might design an inheritance hierarchy:

abstract class Payment {
    // Infrastructure methods
    protected void logTransaction(String details) { /* ... */ }
    protected void detectFraud() { /* ... */ }
    protected void sendNotification() { /* ... */ }

    // Template method pattern
    public final void process() {
        validate();
        detectFraud();
        executePayment();
        logTransaction("Payment processed");
        sendNotification();
    }

    // Subclasses implement these
    protected abstract void validate();
    protected abstract void executePayment();
}

class CreditCardPayment extends Payment {
    protected void validate() {
        // Credit card validation
    }
    protected void executePayment() {
        // Credit card processing
    }
}

class PayPalPayment extends Payment {
    protected void validate() {
        // PayPal validation
    }
    protected void executePayment() {
        // PayPal processing
    }
}
Enter fullscreen mode Exit fullscreen mode

This seems reasonable! We're reusing code, following OOP principles, right?

Identifying the Independent Change Drivers

PIV asks us to identify the change drivers—the reasons our code will need to change:

1. Payment Method Variety Driver

New payment methods emerge constantly:

  • Credit cards
  • PayPal
  • Cryptocurrency
  • Bank transfers
  • Buy-now-pay-later services

Each has unique validation rules, processing logic, and external integrations. Crucially, adding cryptocurrency support should not require modifying credit card processing.

2. Common Payment Infrastructure Driver

Cross-cutting concerns that apply to all payments:

  • Transaction logging
  • Fraud detection
  • Retry logic
  • Audit trails
  • Notification systems

These evolve for entirely different reasons: performance optimization, security updates, observability improvements.

Key observation: These drivers vary independently.

Payment methods change for business reasons (new providers, fee structures, regulations).

Infrastructure changes for technical reasons (performance, security, monitoring).

The Problem with Inheritance

Our inheritance approach couples these independent drivers:

❌ Tight Coupling

  • Every payment method depends on the Payment base class
  • Infrastructure changes force recompilation of ALL payment methods
  • Even though their payment logic hasn't changed!

❌ Low Cohesion

Each payment subclass mixes multiple concerns:

  • Payment method logic (validation, execution)
  • Infrastructure usage (logging, fraud detection)

A developer working on credit card processing must understand logging infrastructure. An infrastructure specialist modifying fraud detection must understand all payment implementations.

❌ Rigidity

The template method enforces a fixed execution sequence. What if PayPal requires fraud detection after payment execution, not before? The base class structure must change, affecting all payment methods.

❌ Poor Testability

Testing CreditCardPayment requires instantiating the entire Payment hierarchy, including all infrastructure dependencies. You cannot isolate payment logic from infrastructure.

❌ The Fragile Base Class Problem

The base class becomes a dumping ground for shared functionality. As more payment methods are added, developers add more protected methods, creating an ever-growing, unfocused base class.

The Composition Solution

Now let's decouple those independent drivers:

// Payment method interface (domain abstraction)
interface PaymentMethod {
    void validate();
    PaymentResult executePayment();
}

// Concrete payment methods (vary independently)
class CreditCardPayment implements PaymentMethod {
    public void validate() {
        // Credit card validation
    }

    public PaymentResult executePayment() {
        // Credit card processing
    }
}

class PayPalPayment implements PaymentMethod {
    public void validate() {
        // PayPal validation
    }

    public PaymentResult executePayment() {
        // PayPal processing
    }
}

// Infrastructure services (vary independently)
interface TransactionLogger {
    void log(String details);
}

interface FraudDetector {
    boolean analyze(PaymentMethod method);
}

interface NotificationService {
    void send(String message);
}

// Payment processor composes infrastructure and payment method
class PaymentProcessor {
    private final TransactionLogger logger;
    private final FraudDetector fraudDetector;
    private final NotificationService notifier;

    public PaymentProcessor(TransactionLogger logger,
                           FraudDetector fraudDetector,
                           NotificationService notifier) {
        this.logger = logger;
        this.fraudDetector = fraudDetector;
        this.notifier = notifier;
    }

    public void process(PaymentMethod method) {
        method.validate();

        if (fraudDetector.analyze(method)) {
            PaymentResult result = method.executePayment();
            logger.log("Payment processed: " + result);
            notifier.send("Payment confirmation");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The Benefits of Decoupling

✅ Independent Variation

  • Add payment methods without touching infrastructure: Just implement PaymentMethod
  • Improve infrastructure without touching payment methods: Just update the service implementation

✅ High Cohesion

Each class has one responsibility:

  • CreditCardPayment handles credit card logic only
  • TransactionLogger handles logging only
  • PaymentProcessor handles orchestration only

✅ Flexible Orchestration

Need fraud detection after execution for PayPal? Create a PayPalPaymentProcessor variant. Payment method implementations remain unchanged.

✅ Excellent Testability

  • Test CreditCardPayment with zero infrastructure dependencies
  • Mock TransactionLogger and FraudDetector to test orchestration in isolation
  • Each concern is independently verifiable

✅ Explicit Dependencies

The PaymentProcessor constructor declares its infrastructure dependencies. No hidden inheritance magic. Reading the constructor reveals all concerns immediately.

So When IS Inheritance Appropriate?

PIV doesn't say "never use inheritance." It says: use inheritance when concerns vary dependently—when they always change together.

Appropriate use cases:

✓ True "is-a" Relationships

Geometric shapes with shared mathematical invariants:

abstract class Shape {
    abstract double area();
    abstract double perimeter();
}

class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

The shape concept and its implementations represent a unified concern that evolves together.

✓ Framework Extension Points

Stable, carefully designed template methods:

class HttpServlet {
    // Stable framework lifecycle
    public void service(HttpRequest req, HttpResponse res) {
        if (req.getMethod().equals("GET")) {
            doGet(req, res);
        } else if (req.getMethod().equals("POST")) {
            doPost(req, res);
        }
    }

    protected void doGet(...) { }
    protected void doPost(...) { }
}
Enter fullscreen mode Exit fullscreen mode

The framework's request handling lifecycle is stable and well-designed. Subclasses extend specific, intentional extension points.

✓ Stable Domain Hierarchies

Well-established taxonomies that evolve slowly:

abstract class LegalEntity { /* ... */ }
class Person extends LegalEntity { /* ... */ }
class Company extends LegalEntity { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

The domain taxonomy represents fundamental knowledge that doesn't vary independently.

The Fundamental Pattern

PIV reveals a consistent pattern across design decisions:

When independent change drivers are separated → cohesion increases

When independent change drivers are coupled → cohesion decreases

This isn't just about inheritance vs composition. The same pattern explains:

  • Why Dependency Inversion works (separating business rules from infrastructure)
  • When immutability is beneficial (separating state evolution from concurrent access)
  • How to choose between recursion and iteration (separating algorithm logic from execution control)

The Decision Framework

When facing the inheritance vs composition choice, ask yourself:

  1. Identify the change drivers: What are the reasons this code will change?

  2. Check independence: Do these drivers vary for different reasons?

    • Business vs technical?
    • Domain vs infrastructure?
    • Different stakeholders?
  3. If independent → Prefer composition

    • Create interfaces for each driver
    • Use dependency injection
    • Keep concerns separated
  4. If dependent → Inheritance may be fine

    • They represent a unified concept
    • They always evolve together
    • The base class provides genuine shared behavior

The Takeaway

"Composition over inheritance" isn't a universal rule—it's a guideline that emerges from a deeper principle:

Separate elements that vary for independent reasons

PIV helps you identify when concerns are independent (use composition) and when they're genuinely unified (inheritance may be appropriate).

The next time you reach for inheritance, pause and ask: "Are these concerns varying independently?"

If yes, composition will give you better decoupling, higher cohesion, and easier maintenance.

If no, inheritance might be the right tool after all.


Learn More

This article is based on research from:

Loth, Y. (2025). The Principle of Independent Variation. Zenodo.
https://doi.org/10.5281/zenodo.17677316

The PIV paper explores this principle across multiple design decisions, providing a unifying framework for understanding why certain design patterns and principles work.


What design decisions have you reconsidered through the lens of independent change drivers? Share your experiences in the comments!

Top comments (0)