Create your own
Lesson illustration

Choosing Between Composition and Inheritance

Hello. In the previous lesson, you assigned behavior to the object that owns the necessary information and invariants, while keeping coordination narrow. That gives us a strong starting point for today’s choice: when two concepts collaborate, should one inherit from the other, or should one contain and delegate to the other?

This distinction is central to maintainable LLD. In an interview, inheritance can make an initial design look elegant, but an interviewer will often add one new requirement specifically to test whether your hierarchy can absorb it. By the end of this lesson, you should be able to choose composition or inheritance from the requirements, explain the trade-off using substitutability and change scenarios, and sketch a Python design that avoids a growing subclass tree.


Two different relationships, not two interchangeable reuse tricks

Both mechanisms reuse code, but they make very different claims about the domain.

  • Inheritance models an is-a relationship. A SalaryPolicy is a PayPolicy; it can stand in wherever code expects a PayPolicy.
  • Composition models a has-a relationship. An Employee has a payroll policy, a work role, and possibly an address.

The important consequence is that inheritance couples a child class to the parent’s public interface, implementation details, and future evolution. A subclass inherits more than the few methods it currently needs. Composition creates a smaller dependency boundary: the containing object collaborates with another object through only the behavior it needs.

A useful default is:

Prefer composition when you are combining independent capabilities or policies. Use inheritance only when there is a genuine subtype relationship and the subtype can safely replace the base type.

This is not “inheritance is bad.” Inheritance is useful when its semantic promise is true. The risk comes from treating it as a shortcut for sharing code.

The Flaws of Inheritance

Watch “The Flaws of Inheritance” by CodeAesthetic for a compact visual explanation of why an apparently tidy base class can become rigid as requirements vary.

Watch the inheritance example, focusing on why a drawable image is forced to implement file-oriented operations it does not meaningfully support. Then watch the composition redesign, where in-memory image data, file formats, and drawing behavior become separate, combinable objects. Notice that the redesign is not merely “more classes”: each class now represents one independently changing concern.

The image example illustrates a key warning sign for inheritance: if a subclass must throw an exception, return a nonsense value, or ignore an inherited method, the parent interface is probably not a valid contract for that subtype.


The deciding test: substitutability, not vocabulary

The phrase “is-a” is helpful, but it is not enough. Many relationships sound plausible in English and still produce a broken design.

The stronger test is the Liskov Substitution Principle:

If client code is written for a base type, replacing a base object with any subtype must preserve the client’s valid expectations.

Suppose a parking system accepts a Vehicle:

def admit(vehicle: Vehicle) -> ParkingTicket:
    spot = find_compatible_spot(vehicle.size)
    return issue_ticket(vehicle.plate, spot)

An ElectricVehicle can reasonably inherit from Vehicle if it preserves the ordinary vehicle contract: it has a plate, a size, and can be admitted to a compatible spot. It may add battery information without changing how existing Vehicle clients behave.

Now consider a misleading use of inheritance:

class Image:
    def resize(self, factor: float) -> None: ...
    def flip_horizontal(self) -> None: ...
    def load(self, path: str) -> None: ...
    def save(self, path: str) -> None: ...

A DrawableImage may want resizing and flipping but have no meaningful load() or save() behavior. If its implementation is:

def save(self, path: str) -> None:
    raise NotImplementedError("Drawable images cannot be saved")

then a caller that receives an Image cannot safely use a DrawableImage. The subtype violates the parent’s contract.

The better decomposition is usually:

  • Image: represents pixels in memory and supports transformations.
  • ImageFile: loads or saves an Image in a particular format.
  • ImageDrawer: draws onto an Image.

These are separate capabilities. A caller can combine them when needed without forcing every image-like object into a file-oriented hierarchy.

A practical substitution checklist

Before introducing class Child(Parent), evaluate these questions:

  1. Can every valid operation on Parent also be performed meaningfully on Child?
    “Meaningfully” excludes methods that throw NotImplementedError, silently do nothing, or reinterpret parameters unexpectedly.

  2. Does the child preserve the parent’s invariants?
    If Parent promises one behavior, the child cannot weaken it. For example, if a Rectangle.resize(length, width) independently changes two dimensions, a Square cannot honor that operation while preserving equal sides.

  3. Are clients likely to use the child wherever they use the parent?
    If not, it is not a sound subtype merely because it shares fields or a formula.

  4. Is the parent intentionally designed for extension?
    A parent with many unrelated methods, exposed internal fields, or unclear override rules is a fragile foundation for subclasses.

  5. Are you inheriting only to reuse a few methods?
    If so, composition and delegation are usually safer.

The “reverse relationship” question can be a useful smell test. If both “a square is a rectangle” and “a rectangle is a square” seem defensible at a vague level, the concepts may be peers with overlapping mathematics rather than parent and child types. Their APIs and invariants, not the wording alone, should decide the design.

Replace Inheritance with Delegation

Read Refactoring Guru’s short “Replace Inheritance with Delegation” guide to connect an inheritance decision to a concrete refactoring move: keep the useful collaborator, remove the invalid parent-child relationship.

In the “Problem” and “Solution” sections, read the problem and solution. Then read the “Why Refactor” discussion, especially the Liskov and unused-method rationale. Focus on the distinction between using a parent as a true abstraction and using it merely as a code container.


Independent axes create the subclass-explosion problem

Return to the employee-management example. Imagine these requirements:

  • Employees have a work role: manager, sales, secretary, or factory worker.
  • Employees have a payment plan: salary, hourly, or contractor.
  • Some employees receive a commission; others do not.
  • A worker’s payment plan can change when their employment status changes.

A hierarchy often starts innocently:

class Employee:
    ...

class SalariedEmployee(Employee):
    ...

class HourlyEmployee(Employee):
    ...

class CommissionSalariedEmployee(SalariedEmployee):
    ...

Then work roles appear:

class SalariedManager(SalariedEmployee):
    ...

Soon, the design needs variants such as:

  • HourlyFactoryWorker
  • CommissionSalariedSalesPerson
  • ContractorSalesPerson
  • CommissionHourlySalesPerson

The problem is not simply “there are many classes.” The problem is that role, pay plan, and incentive are independent dimensions of variation. Modeling every valid combination as a subtype creates a class for each combination.

If there are role options, payment options, and incentive options, a hierarchy may trend toward:

concrete employee classes.

Composition instead models each dimension once:

  • several role objects,
  • several payment-plan objects,
  • several incentive objects,
  • one Employee that combines appropriate objects.

The number of component implementations grows more like:

That is why composition is especially valuable when requirements contain phrases such as:

  • “can independently be configured”
  • “may be changed later”
  • “optionally has”
  • “can be combined with any”
  • “supports different rules by customer, region, or plan”

These phrases signal separate axes of behavior, not a single taxonomy.

Inheritance and Composition: A Python OOP Guide – Real Python

Read the targeted portions of Real Python’s guide for the employee example that moves from a growing inheritance hierarchy to policy-based composition.

First, in “The Class Explosion Problem,” read the expanding employee hierarchy. Then move to “Flexible Designs With Composition.” Read the policy-based redesign. Pay particular attention to why role and payroll are independent collaborators of Employee, while payroll-policy implementations can still share a focused abstraction.

The following UML diagram captures this composition-based design.

An `Employee` contains an `Address`, an `IRole`, and an `IPayrollCalculator`; distinct role and payroll-policy implementations provide interchangeable behavior. The black diamonds indicate the employee’s composed relationships, while the hollow triangles show inheritance within the focused payroll-policy family.

Notice the nuanced design choice in the diagram:

  • Employee composes a role and a payroll calculator because these capabilities vary independently for each employee.
  • HourlyPolicy, SalaryPolicy, and CommissionPolicy may use inheritance within the narrower payroll-policy family because each is genuinely a kind of payroll policy and supports the same calculation responsibility.

So “prefer composition” does not mean “never use inheritance anywhere.” It means do not force unrelated variation into one hierarchy.


A Python design: compose the employee, specialize focused policies

Here is a compact design that keeps identity, work role, pay plan, and commission separate:

from dataclasses import dataclass
from typing import Optional


class PayPlan:
    def pay_for(self, hours_worked: int) -> int:
        raise NotImplementedError


@dataclass(frozen=True)
class SalaryPlan(PayPlan):
    weekly_salary: int

    def pay_for(self, hours_worked: int) -> int:
        return self.weekly_salary


@dataclass(frozen=True)
class HourlyPlan(PayPlan):
    hourly_rate: int

    def pay_for(self, hours_worked: int) -> int:
        return self.hourly_rate * hours_worked


@dataclass(frozen=True)
class Commission:
    sales_count: int
    amount_per_sale: int

    def bonus(self) -> int:
        return self.sales_count * self.amount_per_sale


class SalesRole:
    def perform_duties(self, hours: int) -> str:
        return f"Contacted prospects for {hours} hours."


class FactoryRole:
    def perform_duties(self, hours: int) -> str:
        return f"Manufactured products for {hours} hours."


@dataclass
class Employee:
    employee_id: str
    name: str
    role: object
    pay_plan: PayPlan
    commission: Optional[Commission] = None

    def work(self, hours: int) -> str:
        return self.role.perform_duties(hours)

    def calculate_pay(self, hours_worked: int) -> int:
        total = self.pay_plan.pay_for(hours_worked)
        if self.commission is not None:
            total += self.commission.bonus()
        return total

A single employee can now be assembled without creating a special subtype:

sales_employee = Employee(
    employee_id="E-42",
    name="Sam",
    role=SalesRole(),
    pay_plan=HourlyPlan(hourly_rate=60),
    commission=Commission(sales_count=8, amount_per_sale=100),
)

assert sales_employee.calculate_pay(hours_worked=40) == 3200

This avoids creating a class called CommissionHourlySalesEmployee. More importantly, it reflects the domain: being in sales does not inherently dictate being salaried, hourly, or commissioned.

If Sam changes temporarily from hourly to salaried employment, the application can replace the collaborator:

sales_employee.pay_plan = SalaryPlan(weekly_salary=2800)

The employee’s identity and work role remain unchanged. Only the policy that changed is replaced.

In production code, you would normally formalize the expected role and pay_plan operations using a Protocol or abstract interface, validate monetary values carefully, and control who may replace a plan. The key design decision comes first: the employee should not become a new subtype merely because one independently variable policy changed.


Composition has costs too

Composition is flexible, but it is not automatically better in every situation.

Its costs include:

  • More objects and construction wiring. A composed object may need several collaborators.
  • Delegating methods. The containing object may expose a method that forwards work to a component.
  • More relationships to understand. If overdone, a simple flow becomes hidden among many tiny abstractions.
  • Potentially invalid combinations. If a requirement says contractors cannot receive a particular incentive, object construction or domain validation must enforce that rule.

Do not introduce a hierarchy of strategies for a rule that will never vary. For example, if every parking ticket has one fixed fee formula and the prompt gives no likely pricing changes, a direct calculate_fee() method can be clearer than a PricingPolicy abstraction.

The right question is not “Can I make this configurable?” It is:

Is this behavior a likely independent source of change, and can valid objects combine it in multiple ways?

If the answer is no, keep the design simpler.

A concise interview decision framework

When an interviewer asks you to choose, narrate your reasoning in this order:

  1. Name the relationship.
    “An employee has a pay plan; it is not itself a kind of pay plan.”

  2. Identify the changing axes.
    “Role, payment method, and incentives can vary independently.”

  3. Apply substitutability.
    “A sales employee should not have to inherit methods or constraints from a salaried employee merely to gain sales behavior.”

  4. Choose the structure.
    “I will compose Employee with role and pay-plan collaborators, then delegate to them.”

  5. Preserve narrowly valid inheritance.
    “Concrete pay plans may implement a common pay-plan abstraction, because all of them support the same pay-calculation contract.”

  6. State the trade-off.
    “This adds object wiring, but avoids a multiplying number of employee subclasses and permits policy replacement.”

A strong, interview-ready justification would be:

“I am choosing composition because role, compensation, and incentives are independent change axes. Modeling their combinations as employee subclasses would create a growing hierarchy and tightly couple employment identity to policy details. Employee will hold collaborators for these behaviors and delegate to them. I would reserve inheritance for a small family whose members safely satisfy the same contract, such as concrete implementations of a payroll-policy abstraction.”


Key takeaways

Inheritance and composition answer different design questions:

  • Use inheritance for a clear subtype that can safely replace the base type and meaningfully supports its full contract.
  • Use composition when an object has independently variable data, capabilities, policies, or collaborators.
  • Do not inherit merely to reuse a few methods. That often imports an invalid interface and creates tight coupling.
  • Watch for subclass explosion when multiple independent dimensions, such as role and payment plan, are modeled in the same hierarchy.
  • A design can use both: composition at the domain-object level and limited inheritance among closely related policy implementations.
  • In interviews, justify the choice using change scenarios, substitutability, and the trade-off between flexibility and extra wiring.

Next, you will make these composed designs precise by learning to specify clear contracts for classes and interfaces. That will let Employee depend on exactly what a role or payroll policy promises to do, rather than on a particular concrete implementation.

Can't find a good explanation? Sign up and we'll make it for you

Sign up