In object-oriented design, few principles have aged as well as:
Favor composition over inheritance.
Popularized in Design Patterns, this principle remains foundational across modern ecosystems — from JVM languages to .NET, Python, and Swift.
While inheritance models taxonomy, composition models behavior. In practice, behavior modeling is what most real systems need.
This article explains why composition is preferred, and demonstrates it with clean, minimal examples in:
- Kotlin
- Java
- Python
- C#
- Swift
Why Composition Wins in Real Systems
Inheritance introduces:
- Tight coupling between parent and child
- Fragile base class problems
- Rigid hierarchies
- Reduced testability
Composition provides:
- Loose coupling
- Better encapsulation
- Runtime flexibility
- Improved unit testing
- Cleaner domain modeling
The guiding rule:
Use inheritance for “is-a” relationships.
Use composition for “has-a” relationships.
The Example: A Car and an Engine
A car has an engine.
A car is not an engine.
Let’s model that correctly in each language.
Kotlin
Kotlin encourages composition by making classes final by default.
❌ Inheritance (Incorrect Modeling)
open class Engine {
fun start() = println("Engine starting")
}
class Car : Engine() {
fun drive() {
start()
println("Driving")
}
}
This implies Car is an Engine, which is semantically wrong.
✅ Composition (Preferred)
class Engine {
fun start() = println("Engine starting")
}
class Car(private val engine: Engine) {
fun drive() {
engine.start()
println("Driving")
}
}
Now:
- Car has an Engine
- Engine can be replaced
- Easier testing via dependency injection
Java
Modern Java architecture (especially with Spring) strongly favors composition.
❌ Inheritance
class Engine {
void start() {
System.out.println("Engine starting");
}
}
class Car extends Engine {
void drive() {
start();
System.out.println("Driving");
}
}
Poor semantic relationship.
✅ Composition
class Engine {
void start() {
System.out.println("Engine starting");
}
}
class Car {
private final Engine engine;
Car(Engine engine) {
this.engine = engine;
}
void drive() {
engine.start();
System.out.println("Driving");
}
}
This enables:
- Dependency injection
- Swappable implementations
- Test doubles in unit tests
Python
Python’s flexibility makes composition natural and expressive.
❌ Inheritance
class Engine:
def start(self):
print("Engine starting")
class Car(Engine):
def drive(self):
self.start()
print("Driving")
This blurs domain boundaries.
✅ Composition
class Engine:
def start(self):
print("Engine starting")
class Car:
def __init__(self, engine):
self.engine = engine
def drive(self):
self.engine.start()
print("Driving")
Python benefits especially from composition because:
- Duck typing allows flexible substitution
- Testing with mocks is trivial
- No rigid type hierarchy required
C# (.NET)
Modern .NET (ASP.NET Core and Clean Architecture) is composition-first.
❌ Inheritance
class Engine
{
public void Start() => Console.WriteLine("Engine starting");
}
class Car : Engine
{
public void Drive()
{
Start();
Console.WriteLine("Driving");
}
}
Incorrect domain modeling.
✅ Composition
class Engine
{
public void Start() => Console.WriteLine("Engine starting");
}
class Car
{
private readonly Engine _engine;
public Car(Engine engine)
{
_engine = engine;
}
public void Drive()
{
_engine.Start();
Console.WriteLine("Driving");
}
}
This pattern scales naturally into:
- Interface-based design
- Dependency Injection containers
- Testable services
Swift
Swift is arguably the most composition-oriented mainstream language.
❌ Inheritance
class Engine {
func start() {
print("Engine starting")
}
}
class Car: Engine {
func drive() {
start()
print("Driving")
}
}
Incorrect subtype semantics.
✅ Composition
class Engine {
func start() {
print("Engine starting")
}
}
class Car {
private let engine: Engine
init(engine: Engine) {
self.engine = engine
}
func drive() {
engine.start()
print("Driving")
}
}
Even more idiomatic Swift uses protocols:
protocol Engine {
func start()
}
This avoids inheritance entirely.
Architectural Impact
Composition unlocks:
1. Testability
You can inject mock dependencies easily:
Car car = new Car(new FakeEngine());
2. Flexibility
Swap behavior without changing class hierarchy.
3. Clear Domain Modeling
A Car having an Engine is correct.
A Car being an Engine is not.
4. Better SOLID Compliance
Composition naturally supports:
- Single Responsibility Principle
- Open/Closed Principle
- Dependency Inversion Principle
When Inheritance Is Appropriate
Inheritance still has valid use cases:
- True subtype relationships
- Template Method pattern
- Framework extension points
- Sealed hierarchies
But deep inheritance trees are often a code smell.
Final Takeaway
Across Kotlin, Java, Python, C#, and Swift, one principle consistently produces better systems:
Design behavior through composition.
Inheritance defines what something is.
Composition defines what something does.
Modern software systems are behavior-driven. That’s why composition dominates contemporary architecture.

