DEV Community

Parth Kamal
Parth Kamal

Posted on

Composition vs Inheritance — The Practical Guide Every Developer Should Know

Favor Composition Over Inheritance

Every developer hears this advice at some point: “Favor composition over inheritance.”
But very few really internalize when and why that matters. Inheritance and composition are both powerful tools in object-oriented design, but they serve different purposes. The key is knowing when to use each — and when not to.


Understanding the Core Idea

At its heart, the difference comes down to the kind of relationship you want to model.
Inheritance represents an “is-a” relationship — for example, a Car is a Vehicle. It means the subclass should conceptually be a specialized form of the parent.
Composition, on the other hand, models a “has-a” relationship — for example, a Car has an Engine. Here, one class simply contains or uses another to get its work done.

This distinction seems simple, but it changes how your code evolves over time.


When to Use Inheritance

Inheritance isn’t bad — it’s just easily abused. It’s the right choice when the relationship between classes truly reflects the idea of specialization. If the child class logically “is” the parent type, and you expect this relationship to remain stable, inheritance fits naturally.

For instance, a Car extending a Vehicle, or a Circle extending an abstract Shape class, makes sense. In both cases, the child shares a clear structure and purpose with the parent, while still providing its own implementation details.

Inheritance also shines when you need polymorphism — treating different subclasses under a single unified interface. A list of Shape objects, for example, can hold circles, rectangles, or triangles, all of which can respond to the same area() method.

Finally, inheritance is often used when frameworks or libraries enforce it, such as when extending HttpServlet or Activity in Java. In such cases, it’s part of the system’s design contract.

However, you should avoid inheritance when your goal is merely to reuse code, when the base class might change frequently, or when the relationship doesn’t make real-world sense. Extending a class just because you need a few of its methods leads to tight coupling and fragile hierarchies that are difficult to maintain.


When to Use Composition

Composition is the better choice when you want flexibility, loose coupling, and reusability. Instead of saying “X is a Y,” composition says “X uses Y.” That subtle change allows you to modify, replace, or combine behaviors without touching your core classes.

Let’s take a simple example. Instead of creating separate subclasses for PetrolCar, DieselCar, and ElectricCar, you can let the Car class hold an Engine object. By defining an Engine interface and implementing PetrolEngine, DieselEngine, and ElectricEngine, the Car can use any of them interchangeably. Even better, you can switch the engine type at runtime without changing the class structure.

This approach keeps your code modular and open to extension. You can add new types of engines later without modifying the Car class at all. Composition also helps encapsulate details — the Car doesn’t need to know how the engine starts; it just delegates that responsibility to the engine object.


The Example: Car and Engine

Here’s what that looks like in practice.

Using inheritance

class Car { 
    void start() {} 
}

class PetrolCar extends Car {
    void start() { 
        System.out.println("Starting petrol car"); 
    }
}

class DieselCar extends Car {
    void start() { 
        System.out.println("Starting diesel car"); 
    }
}
Enter fullscreen mode Exit fullscreen mode

This works, but quickly becomes messy. Every new engine type requires a new subclass, and the code becomes rigid and repetitive.

Using composition

interface Engine { 
    void start(); 
}

class PetrolEngine implements Engine {
    public void start() { 
        System.out.println("Starting petrol engine..."); 
    }
}

class DieselEngine implements Engine {
    public void start() { 
        System.out.println("Starting diesel engine..."); 
    }
}

class Car {
    private Engine engine;

    Car(Engine engine) { 
        this.engine = engine; 
    }

    void start() { 
        engine.start(); 
    }

    void setEngine(Engine engine) { 
        this.engine = engine; 
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, the Car class doesn’t care what kind of engine it has — it just knows that whatever engine it holds can start. You can pass a PetrolEngine, change it to a DieselEngine later, or even introduce a new ElectricEngine class in the future without touching the Car implementation.


Choosing Between Them

Inheritance creates tight coupling between classes. Changes in the parent can easily ripple through to all children. It’s good for modeling stable “type” relationships but becomes a burden when behavior needs to evolve. Composition, by contrast, keeps classes independent. It’s easier to test, extend, and refactor. It’s the preferred choice for most cases where you want flexibility and code reuse rather than a rigid type hierarchy.

A simple way to remember it is this:
Use inheritance when you’re defining what something is.
Use composition when you’re defining what something does or has.


Final Takeaway

Both inheritance and composition have their place in good design. Inheritance gives you structure and identity, while composition gives you flexibility and adaptability. The most maintainable systems tend to combine both — using inheritance for stable abstractions and composition for dynamic behavior.

In short:

Use inheritance for identity. Use composition for flexibility.

Top comments (0)