Composition over Inheritance: When and Why

Updated 2026-03-11

TL;DR

Composition over inheritance means building complex objects by combining simpler objects (has-a relationships) rather than inheriting from parent classes (is-a relationships). This approach provides greater flexibility, reduces tight coupling, and makes code easier to test and maintain. While inheritance creates rigid hierarchies, composition allows you to change behavior at runtime by swapping components.

Prerequisites: Understanding of classes and objects, basic inheritance concepts (parent/child classes, method overriding), familiarity with instance variables and methods in Python. Knowledge of what coupling means in software design is helpful but not required.

After this topic: Identify situations where composition is preferable to inheritance, refactor inheritance-based designs into composition-based designs, implement flexible object relationships using composition, and explain the trade-offs between composition and inheritance in technical interviews.

Core Concept

What is Composition?

Composition is a design principle where you build complex objects by combining simpler, independent objects rather than inheriting behavior from parent classes. Instead of saying “a Car is-a Vehicle,” you say “a Car has-an Engine.”

In composition, one class contains references to objects of other classes as instance variables. These contained objects provide the functionality, and the containing class delegates work to them.

Why Favor Composition?

Flexibility: You can change behavior at runtime by swapping out components. With inheritance, behavior is fixed at compile time.

Reduced Coupling: Classes depend on interfaces/abstractions rather than concrete parent classes. This makes changes easier because modifying a parent class won’t cascade through all children.

Better Testability: You can inject mock objects for testing. With inheritance, you’re stuck with parent class behavior unless you override everything.

Avoids Deep Hierarchies: Inheritance can create fragile, hard-to-understand class trees. Composition keeps relationships flat and explicit.

When to Use Each

Use Inheritance when:

  • You have a true “is-a” relationship (a Dog is-a Animal)
  • You need polymorphism (treating different types uniformly)
  • The parent class is designed for extension (like abstract base classes)

Use Composition when:

  • You have a “has-a” or “uses-a” relationship (a Car has-an Engine)
  • You need to change behavior at runtime
  • You want to reuse code from multiple sources (Python doesn’t support multiple inheritance well)
  • You’re combining behaviors from different domains

The Core Principle

The Gang of Four stated it best: “Favor object composition over class inheritance.” This doesn’t mean never use inheritance—it means composition should be your default choice, and you should have a good reason to choose inheritance instead.

Visual Guide

Inheritance vs Composition Structure

graph TD
    A[Inheritance Approach] --> B[Vehicle]
    B --> C[Car]
    B --> D[Truck]
    B --> E[Motorcycle]
    
    F[Composition Approach] --> G[Car]
    G -.has-a.-> H[Engine]
    G -.has-a.-> I[Transmission]
    G -.has-a.-> J[Wheels]
    
    style A fill:#ffcccc
    style F fill:#ccffcc
    style B fill:#ffe6e6
    style G fill:#e6ffe6

Inheritance creates a rigid hierarchy (red), while composition creates flexible relationships (green) where components can be easily swapped or reused.

Runtime Flexibility with Composition

graph LR
    A[Robot] --> B[WalkBehavior]
    A --> C[TalkBehavior]
    
    B -.can swap to.-> D[FlyBehavior]
    C -.can swap to.-> E[SilentBehavior]
    
    style A fill:#lightblue
    style B fill:#lightgreen
    style C fill:#lightgreen
    style D fill:#yellow
    style E fill:#yellow

With composition, you can change a Robot’s behaviors at runtime by swapping component objects. Inheritance would require creating new subclasses for each combination.

Examples

Example 1: Refactoring from Inheritance to Composition

Problem: You’re building a game with different character types. Using inheritance creates an explosion of subclasses.

Inheritance Approach (Problematic):

class Character:
    def __init__(self, name):
        self.name = name
        self.health = 100

class Warrior(Character):
    def attack(self):
        return "Swings sword for 20 damage"

class Mage(Character):
    def attack(self):
        return "Casts fireball for 30 damage"

class HealingWarrior(Character):  # What if we need a warrior who can heal?
    def attack(self):
        return "Swings sword for 20 damage"
    
    def heal(self):
        return "Heals for 15 HP"

class HealingMage(Character):  # Now we need this too!
    def attack(self):
        return "Casts fireball for 30 damage"
    
    def heal(self):
        return "Heals for 15 HP"

# This gets out of control quickly!

Composition Approach (Better):

# Define behavior components
class AttackBehavior:
    def execute(self):
        raise NotImplementedError

class SwordAttack(AttackBehavior):
    def execute(self):
        return "Swings sword for 20 damage"

class MagicAttack(AttackBehavior):
    def execute(self):
        return "Casts fireball for 30 damage"

class HealBehavior:
    def execute(self):
        return "Heals for 15 HP"

class NoHeal:
    def execute(self):
        return "Cannot heal"

# Character uses composition
class Character:
    def __init__(self, name, attack_behavior, heal_behavior):
        self.name = name
        self.health = 100
        self.attack_behavior = attack_behavior
        self.heal_behavior = heal_behavior
    
    def attack(self):
        return self.attack_behavior.execute()
    
    def heal(self):
        return self.heal_behavior.execute()
    
    def set_attack_behavior(self, behavior):
        """Change behavior at runtime!"""
        self.attack_behavior = behavior

# Usage
warrior = Character("Conan", SwordAttack(), NoHeal())
print(warrior.attack())  # Output: Swings sword for 20 damage
print(warrior.heal())    # Output: Cannot heal

# Create a healing warrior by mixing behaviors
healing_warrior = Character("Paladin", SwordAttack(), HealBehavior())
print(healing_warrior.attack())  # Output: Swings sword for 20 damage
print(healing_warrior.heal())    # Output: Heals for 15 HP

# Change behavior at runtime!
warrior.set_attack_behavior(MagicAttack())
print(warrior.attack())  # Output: Casts fireball for 30 damage

Try it yourself: Add a DefenseBehavior component with ShieldBlock and Dodge implementations. Create a character that can change all three behaviors at runtime.

Example 2: Real-World Scenario - Payment Processing

Problem: An e-commerce system needs to handle different payment methods and apply various discounts.

# Composition approach
class PaymentMethod:
    def process(self, amount):
        raise NotImplementedError

class CreditCard(PaymentMethod):
    def __init__(self, card_number):
        self.card_number = card_number
    
    def process(self, amount):
        return f"Charged ${amount} to card ending in {self.card_number[-4:]}"

class PayPal(PaymentMethod):
    def __init__(self, email):
        self.email = email
    
    def process(self, amount):
        return f"Charged ${amount} to PayPal account {self.email}"

class DiscountStrategy:
    def apply(self, amount):
        return amount

class PercentageDiscount(DiscountStrategy):
    def __init__(self, percentage):
        self.percentage = percentage
    
    def apply(self, amount):
        return amount * (1 - self.percentage / 100)

class FixedDiscount(DiscountStrategy):
    def __init__(self, discount):
        self.discount = discount
    
    def apply(self, amount):
        return max(0, amount - self.discount)

# Order uses composition to combine payment and discount
class Order:
    def __init__(self, items, payment_method, discount_strategy):
        self.items = items
        self.payment_method = payment_method
        self.discount_strategy = discount_strategy
    
    def checkout(self):
        total = sum(item['price'] for item in self.items)
        discounted_total = self.discount_strategy.apply(total)
        result = self.payment_method.process(discounted_total)
        return f"{result} (Original: ${total}, Final: ${discounted_total})"

# Usage
items = [{'name': 'Book', 'price': 20}, {'name': 'Pen', 'price': 5}]

# Order with credit card and 10% discount
order1 = Order(
    items,
    CreditCard("1234567890123456"),
    PercentageDiscount(10)
)
print(order1.checkout())
# Output: Charged $22.5 to card ending in 3456 (Original: $25, Final: $22.5)

# Order with PayPal and $5 off
order2 = Order(
    items,
    PayPal("user@example.com"),
    FixedDiscount(5)
)
print(order2.checkout())
# Output: Charged $20 to PayPal account user@example.com (Original: $25, Final: $20)

# Easy to add new payment methods or discounts without changing Order class!

Java/C++ Note: In Java, you’d use interfaces (interface PaymentMethod) instead of base classes. In C++, you’d use abstract base classes with pure virtual functions. The composition pattern works identically across all three languages.

Try it yourself: Add a CryptoCurrency payment method and a BuyOneGetOne discount strategy. Notice how you don’t need to modify the Order class at all.

Common Mistakes

1. Using Composition When Inheritance is Appropriate

Mistake: Forcing composition for true “is-a” relationships.

# BAD: Dog clearly IS-A Animal
class Animal:
    def breathe(self):
        return "Breathing"

class Dog:
    def __init__(self):
        self.animal = Animal()  # Awkward!
    
    def breathe(self):
        return self.animal.breathe()

# GOOD: Use inheritance for true is-a relationships
class Dog(Animal):
    def bark(self):
        return "Woof!"

Why it’s wrong: You lose polymorphism benefits. You can’t treat a Dog as an Animal in collections or function parameters. Use inheritance when you need to substitute child objects for parent types.

2. Creating Too Many Small Components

Mistake: Over-engineering with excessive composition layers.

# BAD: Too granular
class WheelRotation:
    def rotate(self): pass

class WheelColor:
    def get_color(self): pass

class Wheel:
    def __init__(self):
        self.rotation = WheelRotation()
        self.color = WheelColor()
        # This is overkill for simple properties!

# GOOD: Compose at the right level
class Wheel:
    def __init__(self, size, color):
        self.size = size
        self.color = color
    
    def rotate(self):
        return f"Rotating {self.size} inch wheel"

class Car:
    def __init__(self, engine, wheels):
        self.engine = engine
        self.wheels = wheels  # Compose at component level

Why it’s wrong: Creates unnecessary complexity and makes code harder to understand. Compose at the level of meaningful components, not individual properties.

3. Forgetting to Delegate Methods

Mistake: Not exposing composed object functionality when needed.

# BAD: Users can't access engine features
class Car:
    def __init__(self, engine):
        self._engine = engine  # Private, no delegation
    
    def drive(self):
        return "Driving"

# Users can't check engine status!
car = Car(Engine())
# car.get_engine_status()  # Doesn't exist!

# GOOD: Delegate when appropriate
class Car:
    def __init__(self, engine):
        self.engine = engine
    
    def drive(self):
        return "Driving"
    
    def get_engine_status(self):
        return self.engine.status()  # Delegate to composed object

Why it’s wrong: Composition shouldn’t hide necessary functionality. Provide delegation methods or expose the component when users need access.

4. Not Using Interfaces/Abstract Base Classes

Mistake: Composing concrete classes instead of abstractions.

# BAD: Tightly coupled to concrete class
class Robot:
    def __init__(self):
        self.weapon = Sword()  # Hardcoded to Sword!

# GOOD: Depend on abstraction
from abc import ABC, abstractmethod

class Weapon(ABC):
    @abstractmethod
    def attack(self):
        pass

class Sword(Weapon):
    def attack(self):
        return "Slash!"

class Laser(Weapon):
    def attack(self):
        return "Pew pew!"

class Robot:
    def __init__(self, weapon: Weapon):
        self.weapon = weapon  # Any Weapon works!

Why it’s wrong: Violates the Dependency Inversion Principle. Your class becomes tightly coupled to a specific implementation, losing the flexibility that composition provides.

5. Mixing Composition and Inheritance Incorrectly

Mistake: Inheriting from a class while also composing it.

# BAD: Confusing relationship
class Engine:
    def start(self):
        return "Engine started"

class Car(Engine):  # Inherits from Engine
    def __init__(self):
        self.engine = Engine()  # Also composes Engine?!
        # Which engine.start() do we use?

# GOOD: Choose one approach
class Car:
    def __init__(self, engine):
        self.engine = engine  # Composition only
    
    def start(self):
        return self.engine.start()

Why it’s wrong: Creates ambiguity and confusion. A Car is not an Engine—it has an Engine. Pick the relationship that makes semantic sense and stick with it.

Interview Tips

What Interviewers Look For

1. Recognize the Tradeoff: Be ready to explain when you’d choose composition vs. inheritance. A strong answer: “I’d use composition when I need runtime flexibility or when combining behaviors from different domains. I’d use inheritance for true is-a relationships where I need polymorphism, like a Dog is-a Animal.”

2. Refactoring Skills: Interviewers often present a problematic inheritance hierarchy and ask you to refactor it. Practice identifying these red flags:

  • Deep inheritance trees (more than 2-3 levels)
  • Multiple inheritance for behavior mixing
  • Subclasses that override most parent methods
  • Classes named with “And” (like WarriorAndHealer)

3. Design Pattern Knowledge: Composition is the foundation of many design patterns. Mention these when relevant:

  • Strategy Pattern: Composing different algorithms (like our payment/discount example)
  • Decorator Pattern: Adding behavior by wrapping objects
  • Dependency Injection: Passing dependencies as composed objects

Common Interview Questions

Q: “Why is composition more flexible than inheritance?”

Good answer: “Composition allows runtime behavior changes by swapping components, while inheritance locks behavior at compile time. For example, I can change a Robot’s weapon during gameplay with composition, but with inheritance, I’d need to create a new Robot subclass for each weapon type.”

Q: “Give an example where inheritance is better than composition.”

Good answer: “When you have a true is-a relationship and need polymorphism. For instance, if I’m building a shape drawing system, Circle and Rectangle should inherit from Shape so I can store them in a list and call draw() on each. The Liskov Substitution Principle applies here—any Shape subclass should work wherever Shape is expected.”

Q: “How do you test composed objects?”

Good answer: “Composition makes testing easier through dependency injection. I can inject mock objects for each component. For example, when testing a Car class, I can inject a MockEngine that simulates different states without needing a real engine.”

Code Interview Scenario

If asked to design a system, follow this approach:

  1. Identify entities: “We have Users, Products, and Orders.”
  2. Identify behaviors: “Orders need payment processing and shipping.”
  3. Choose composition for behaviors: “I’ll compose PaymentProcessor and ShippingService into Order rather than creating OrderWithCreditCard, OrderWithPayPal subclasses.”
  4. Justify your choice: “This lets us add new payment methods without changing Order, and we can test Order with mock processors.”

Red Flags to Avoid

  • Don’t say “always use composition” or “never use inheritance”—it’s about choosing the right tool
  • Don’t confuse composition with aggregation (composition implies ownership and lifecycle management)
  • Don’t forget to mention testing benefits—interviewers love testability discussions

Power Phrases

  • “This design violates the Open/Closed Principle—composition would let us extend without modifying.”
  • “I’d inject this dependency to enable testing and runtime flexibility.”
  • “This is a has-a relationship, not an is-a relationship, so composition fits better.”

Key Takeaways

  • Composition builds objects by combining components (has-a), while inheritance creates hierarchies (is-a). Choose composition by default for flexibility and reduced coupling.

  • Composition enables runtime behavior changes by swapping components, while inheritance locks behavior at compile time. This makes composed systems more adaptable to changing requirements.

  • Use inheritance for true is-a relationships where you need polymorphism (treating different types uniformly), and use composition for has-a relationships or when combining behaviors from different domains.

  • Compose with abstractions, not concrete classes. Depend on interfaces or abstract base classes so you can swap implementations without changing the containing class.

  • Composition improves testability through dependency injection—you can inject mock components for testing without complex inheritance hierarchies or test doubles.