Skip to main content

Composition vs. Inheritance

Medium

Interview Question: "What's the fundamental difference between composition and inheritance? Why is the industry consensus to 'favor composition over inheritance', and how do Association, Aggregation, and Composition differ?"

There are two ways to build a class out of other things: say it IS that thing, or say it HAS that thing.

A SavingsAccount IS a BankAccount (inheritance). A Car HAS an Engine (composition). Both let you reuse code, but they reuse it very differently.

The quick answer​

Inheritance builds an IS-A relationship: a subclass picks up a parent class's fields and methods at compile time. Composition builds a HAS-A relationship: a class is assembled from other independent objects at runtime, usually handed to it rather than created by it. Most modern codebases lean toward composition, because it's easier to change later and harder to break by accident.

INHERITANCE (IS-A HIERARCHY)COMPOSITION (HAS-A CONTAINER)VehicleCarRIGID COMPILE-TIME COUPLINGCarengineEnginestart()PLUGGABLE, SWAPPABLE AT RUNTIME
Inheritance welds classes together at compile time. Composition assembles independent objects at runtime.

Core comparison​

Inheritance (IS-A)Composition (HAS-A)
RelationshipSavingsAccount is a BankAccountCar has an Engine
CouplingTight: the subclass depends on the parent's internalsLoose: talks to the other object only through its public methods
When it's fixedCompile-time; the hierarchy can't change while runningRuntime; you can swap one component for another on the fly
How polymorphism shows upThrough the class hierarchy (subtyping)Through interfaces or injected parameters

Why composition usually wins​

  1. It's easy to test. If a class has a dependency instead of extending it, you can hand it a fake version in tests. A PaymentService that receives a PaymentGateway through its constructor can be given a real StripeGateway in production and a MockGateway in tests, with no production code changes.
  2. It avoids the fragile base class problem. In a deep inheritance chain, a small, reasonable-looking change to a protected method in the base class can quietly break every subclass that depended on the old behavior. Composition keeps that risk contained, because each piece only talks to the others through a stable interface.
  3. It doesn't box you in. Java and most OOP languages only let you extend one class. Composition has no such limit: a class can hold as many independent collaborators as it needs: a Logger, a DatabasePool, a CacheClient, all at once.

Association, aggregation, composition: the three flavors of "HAS-A"​

These three terms describe the same idea (one object holding a reference to another) but differ in how tightly their lifetimes are tied together.

LIFETIME COUPLING SPECTRUMCustomerOrderASSOCIATIONuses-aneither owns the otherDepartmentProfessorAGGREGATIONweak has-apart outlives the wholeHouseRoomCOMPOSITIONstrong has-apart dies with container
Association, Aggregation, and Composition represent increasing tightness of object lifetimes.
  1. Association ("uses-a"): the loosest link. Two classes just interact: a Customer places an Order. Neither owns the other, and neither one's lifetime depends on the other.
  2. Aggregation (weak HAS-A): a whole/part relationship where the part can live on its own. If a Department shuts down, its Professor objects don't vanish - they still exist in memory, just unattached.
  3. Composition (strong HAS-A): real ownership. The part's lifetime is tied to the whole. If a House object is destroyed, its Room objects go with it.

So is inheritance ever the right call?​

Yes - just not as the default. Reach for inheritance when:

  • The relationship is a genuine, stable IS-A fit in the real domain (not one you're forcing for convenience).
  • It fully satisfies the Liskov Substitution Principle: any subclass instance can stand in for the parent anywhere the parent is expected, without surprising the caller or breaking its assumptions.

Clean Code Example​

Here is how composition with dependency injection is implemented across languages:

// 1. Component Contract
interface Engine {
void start();
}

class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println("Electric motor silently engaged.");
}
}

// 2. Composition (HAS-A): Car HAS an Engine injected via constructor
class Car {
private final Engine engine; // Composed dependency

public Car(Engine engine) {
this.engine = engine; // Loose coupling via constructor injection
}

public void drive() {
engine.start();
System.out.println("Car is moving forward.");
}
}