inheritance
composition
object-oriented programming
software design
programming concepts

Difference between Inheritance and Composition

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Inheritance and composition are two common ways to reuse behavior in object-oriented software, but they create very different change costs. Inheritance builds new types by extending existing ones. Composition builds behavior by connecting smaller objects together. Both are useful, but composition is often the safer default because it keeps dependencies more explicit.

Inheritance Reuses Through Hierarchy

Inheritance models an is-a relationship. A subclass receives state or behavior from a base class and may override parts of it.

python
1class Animal:
2    def move(self) -> str:
3        return "move"
4
5
6class Bird(Animal):
7    def move(self) -> str:
8        return "fly"
9
10
11print(Bird().move())

This is concise, and it works well when the subtype relationship is real and stable. Framework extension points often use inheritance this way. The downside is coupling. Changes to the base class can ripple through subclasses in ways that are not obvious from the call site.

Composition Reuses Through Collaboration

Composition models a has-a relationship. Instead of inheriting behavior, an object delegates to another object that performs one part of the job.

python
1class Engine:
2    def start(self) -> str:
3        return "engine started"
4
5
6class Car:
7    def __init__(self, engine: Engine) -> None:
8        self.engine = engine
9
10    def start(self) -> str:
11        return self.engine.start()
12
13
14print(Car(Engine()).start())

The behavior is assembled from collaborators. That makes dependencies visible and easier to replace. It also reduces the pressure to build deep hierarchies just to share code.

Composition Usually Handles Change Better

Requirements change more often than class hierarchies stay elegant. A subclass may start as a neat specialization and later accumulate exceptions, flags, and overrides because the original base class no longer fits every scenario. Composition tends to localize that change. If one behavior varies, you can replace only that collaborator instead of reorganizing the hierarchy.

That flexibility matters in applications with feature flags, multiple storage backends, or different runtime environments. Inheritance can still support variation, but it often does so by growing more subclasses and making behavior harder to predict.

Testing and Dependency Injection Favor Composition

Composed objects are usually easier to test because collaborators can be replaced with simple fakes.

python
1class FakeEngine:
2    def start(self) -> str:
3        return "fake engine"
4
5
6car = Car(FakeEngine())
7assert car.start() == "fake engine"

This is a natural fit for dependency injection. The code under test receives the behavior it needs through construction instead of relying on base-class internals or subclass overrides. Tests become smaller because the seam is explicit.

When Inheritance Is Still Appropriate

Inheritance is not bad by itself. It is appropriate when the type relationship is genuinely stable and the base class has a focused contract. For example, a framework may define a small abstract base class with a few required methods that all implementations must provide. That is very different from a sprawling base class that tries to share everything.

A useful review question is whether the relationship is truly is-a or whether composition would describe the design more honestly. If the answer is mostly about sharing code rather than expressing a subtype, composition is often the better choice.

Common Pitfalls

The main mistake is using inheritance as the first reuse mechanism because it feels concise at the beginning. Teams then end up with deep hierarchies, hard-to-follow overrides, and broad base classes that know too much. Another common issue is overlooking how much easier testing becomes when behavior is injected through composition instead of inherited implicitly.

Summary

  • Inheritance reuses behavior through a class hierarchy.
  • Composition reuses behavior through explicit collaborating objects.
  • Composition usually offers lower coupling and easier testing.
  • Inheritance works best for stable, focused subtype relationships.
  • Prefer the approach that keeps change isolated and intent clear.

Course illustration
Course illustration

All Rights Reserved.