Composition in OOP: Strong Has-A Relationship

Updated 2026-03-11

TL;DR

Composition is a strong “owns-a” relationship where one object contains other objects as parts, and those parts cannot exist independently without the whole. When the container is destroyed, its parts are destroyed too. This is one of the most fundamental ways to build complex objects from simpler ones.

Prerequisites: Basic understanding of classes and objects, constructor methods (init in Python), instance variables, and basic class instantiation. Familiarity with the concept of object lifecycle (creation and destruction).

After this topic: Implement composition relationships in your code, distinguish composition from other relationships like aggregation and inheritance, design classes that use composition to model real-world “owns-a” relationships, and explain the lifecycle dependency between container and contained objects.

Core Concept

What is Composition?

Composition represents a “whole-part” relationship where the part cannot exist without the whole. Think of a car and its engine: the engine is part of the car, and if you destroy the car, the engine (as part of that specific car) is destroyed too.

In code, composition means one class contains instances of other classes as member variables, and those instances are created when the container is created. The container owns its parts and is responsible for their entire lifecycle.

Key Characteristics

Strong Ownership: The container object owns its parts. The parts are created by the container and exist only as long as the container exists.

Lifecycle Dependency: When the container is destroyed, all its parts are automatically destroyed. There’s no independent existence for the parts.

Exclusive Relationship: Parts typically belong to exactly one container. A heart belongs to one body, not shared between multiple bodies.

Why Composition Matters

Composition is a cornerstone of good object-oriented design because it:

  • Promotes code reuse without the tight coupling of inheritance
  • Models real-world relationships naturally (a house has rooms, a book has pages)
  • Enables encapsulation by hiding complex internal structures
  • Supports the principle “favor composition over inheritance” — a fundamental OOP guideline

Composition vs. Other Relationships

Composition vs. Aggregation: Aggregation is a weaker “has-a” relationship where parts can exist independently. A university has students, but students can exist without that specific university.

Composition vs. Inheritance: Inheritance is an “is-a” relationship. A dog is an animal. Composition is “has-a” or “owns-a”. A dog has a heart.

Composition is generally preferred over inheritance because it’s more flexible and doesn’t create tight coupling between classes.

Visual Guide

Composition Relationship Diagram

classDiagram
    class Car {
        -engine: Engine
        -wheels: List~Wheel~
        +start()
        +drive()
    }
    class Engine {
        -horsepower: int
        +ignite()
    }
    class Wheel {
        -size: int
        +rotate()
    }
    Car *-- Engine : owns
    Car *-- Wheel : owns
    note for Car "Car owns Engine and Wheels.\nThey cannot exist without Car."

Composition is shown with a filled diamond. The Car owns its Engine and Wheels — they are created with the Car and destroyed with it.

Lifecycle Dependency

sequenceDiagram
    participant Client
    participant Car
    participant Engine
    participant Wheel
    
    Client->>Car: create Car()
    activate Car
    Car->>Engine: create Engine()
    activate Engine
    Car->>Wheel: create Wheel()
    activate Wheel
    Car->>Wheel: create Wheel()
    activate Wheel
    Note over Car,Wheel: Car owns these parts
    
    Client->>Car: destroy Car
    Car->>Engine: destroy
    deactivate Engine
    Car->>Wheel: destroy
    deactivate Wheel
    Car->>Wheel: destroy
    deactivate Wheel
    deactivate Car
    Note over Client,Wheel: Parts destroyed with container

When a Car is created, it creates its parts. When the Car is destroyed, all parts are destroyed automatically.

Examples

Example 1: Basic Composition - House and Rooms

class Room:
    def __init__(self, name, area):
        self.name = name
        self.area = area
    
    def __str__(self):
        return f"{self.name} ({self.area} sq ft)"

class House:
    def __init__(self, address):
        self.address = address
        # House OWNS these rooms - they're created here
        self.rooms = [
            Room("Living Room", 300),
            Room("Bedroom", 200),
            Room("Kitchen", 150)
        ]
    
    def get_total_area(self):
        return sum(room.area for room in self.rooms)
    
    def __str__(self):
        room_list = ", ".join(str(room) for room in self.rooms)
        return f"House at {self.address}\nRooms: {room_list}\nTotal: {self.get_total_area()} sq ft"

# Usage
my_house = House("123 Main St")
print(my_house)
# Output:
# House at 123 Main St
# Rooms: Living Room (300 sq ft), Bedroom (200 sq ft), Kitchen (150 sq ft)
# Total: 650 sq ft

# When my_house is deleted, all rooms are deleted too
del my_house  # Rooms are destroyed automatically

Key Points:

  • The House creates Room objects in its constructor
  • Rooms don’t exist before the house is created
  • Rooms can’t be shared between houses
  • When the house is destroyed, rooms are destroyed

Try it yourself: Add a Bathroom room to the house and a method to count rooms by type.


Example 2: Composition with Multiple Levels - Computer System

class CPU:
    def __init__(self, cores, speed_ghz):
        self.cores = cores
        self.speed_ghz = speed_ghz
    
    def process(self):
        return f"Processing on {self.cores} cores at {self.speed_ghz}GHz"

class Memory:
    def __init__(self, size_gb):
        self.size_gb = size_gb
    
    def store(self, data):
        return f"Storing {data} in {self.size_gb}GB memory"

class Motherboard:
    def __init__(self, cpu_cores, cpu_speed, memory_size):
        # Motherboard OWNS CPU and Memory
        self.cpu = CPU(cpu_cores, cpu_speed)
        self.memory = Memory(memory_size)
    
    def boot(self):
        return f"Booting...\n{self.cpu.process()}\n{self.memory.store('OS')}"

class Computer:
    def __init__(self, brand):
        self.brand = brand
        # Computer OWNS Motherboard (which owns CPU and Memory)
        self.motherboard = Motherboard(cpu_cores=8, cpu_speed=3.5, memory_size=16)
    
    def start(self):
        return f"{self.brand} Computer Starting:\n{self.motherboard.boot()}"

# Usage
my_computer = Computer("TechBrand")
print(my_computer.start())
# Output:
# TechBrand Computer Starting:
# Booting...
# Processing on 8 cores at 3.5GHz
# Storing OS in 16GB memory

# Nested composition: Computer owns Motherboard, Motherboard owns CPU and Memory
# When computer is destroyed, everything is destroyed
del my_computer  # Destroys Motherboard, CPU, and Memory

Key Points:

  • Multi-level composition: ComputerMotherboardCPU/Memory
  • Each level owns its parts completely
  • Destruction cascades: destroying Computer destroys all nested parts
  • Parts are created in constructors, not passed in

Try it yourself: Add a HardDrive class to the Motherboard and implement a save_file() method.


Example 3: Composition vs. Aggregation Comparison

# COMPOSITION: Engine belongs to Car exclusively
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

class Car:
    def __init__(self, model):
        self.model = model
        # Car CREATES and OWNS the engine
        self.engine = Engine(250)
    
    def start(self):
        return f"{self.model} starting with {self.engine.horsepower}hp engine"

car = Car("Sedan")
print(car.start())  # Output: Sedan starting with 250hp engine
# Engine cannot exist without this car

# AGGREGATION (for contrast): Driver can exist independently
class Driver:
    def __init__(self, name):
        self.name = name

class Taxi:
    def __init__(self, driver):
        # Taxi USES driver, but doesn't own them
        self.driver = driver  # Driver passed in, not created
    
    def drive(self):
        return f"{self.driver.name} is driving the taxi"

driver = Driver("John")  # Driver exists independently
taxi = Taxi(driver)      # Taxi uses existing driver
print(taxi.drive())      # Output: John is driving the taxi

del taxi  # Taxi destroyed, but driver still exists
print(driver.name)  # Output: John (driver still exists)

Key Differences:

  • Composition: Car creates Engine internally — strong ownership
  • Aggregation: Taxi receives Driver externally — weak relationship
  • Lifecycle: Engine dies with Car; Driver survives Taxi

Try it yourself: Create a Playlist class that owns Song objects (composition) vs. a Library class that references existing songs (aggregation).


Language-Specific Notes

Java:

public class House {
    private List<Room> rooms;  // Composition
    
    public House(String address) {
        this.rooms = new ArrayList<>();
        rooms.add(new Room("Living Room", 300));
        // Rooms are created and owned by House
    }
}

C++:

class House {
private:
    std::vector<Room> rooms;  // Composition by value
    // Rooms are destroyed when House is destroyed
public:
    House(std::string address) {
        rooms.push_back(Room("Living Room", 300));
    }
};

Python’s Garbage Collection: Python automatically destroys objects when references are gone. In languages like C++, you must explicitly manage memory for composed objects.

Common Mistakes

1. Confusing Composition with Aggregation

Mistake: Passing objects into the constructor and calling it composition.

# This is AGGREGATION, not composition
class Car:
    def __init__(self, engine):  # Engine passed in
        self.engine = engine  # Car doesn't own it

# Correct composition:
class Car:
    def __init__(self):
        self.engine = Engine()  # Car creates and owns engine

Why it matters: In interviews, you need to distinguish these relationships. Composition means the container creates and owns the parts.


2. Creating Parts Outside the Container

Mistake: Creating composed objects externally instead of in the constructor.

# Wrong: Parts created outside
engine = Engine()
car = Car()
car.engine = engine  # Not true composition

# Correct: Parts created inside
class Car:
    def __init__(self):
        self.engine = Engine()  # Created internally

Why it matters: True composition means the container controls the entire lifecycle of its parts. External creation breaks this contract.


3. Allowing Parts to Outlive the Container

Mistake: Returning references to internal parts that can be stored elsewhere.

class House:
    def __init__(self):
        self.rooms = [Room("Bedroom")]
    
    def get_rooms(self):
        return self.rooms  # Returns reference!

# Problem:
house = House()
rooms = house.get_rooms()  # rooms now references internal list
del house  # House destroyed, but rooms list still accessible

Better approach: Return copies or use read-only access.

def get_rooms(self):
    return self.rooms.copy()  # Return a copy

4. Overusing Composition When Inheritance Makes Sense

Mistake: Using composition for “is-a” relationships.

# Wrong: Dog is-a Animal, not has-a Animal
class Dog:
    def __init__(self):
        self.animal = Animal()  # Awkward

# Correct: Use inheritance for is-a
class Dog(Animal):
    pass

Rule of thumb: Use composition for “has-a” or “owns-a”, inheritance for “is-a”.


5. Not Considering Deep vs. Shallow Copying

Mistake: Shallow copying composed objects when you need deep copies.

import copy

class House:
    def __init__(self):
        self.rooms = [Room("Bedroom")]

# Shallow copy problem:
house1 = House()
house2 = copy.copy(house1)  # Shallow copy
house2.rooms[0].name = "Kitchen"  # Modifies house1's room too!

# Solution: Deep copy when needed
house2 = copy.deepcopy(house1)  # Now they're independent

Why it matters: In composition, you often need to ensure complete independence between objects, especially when copying.

Interview Tips

1. Be Ready to Explain the Difference

Common question: “What’s the difference between composition and inheritance?”

Strong answer structure:

  • Composition: “has-a” relationship, flexible, loose coupling
  • Inheritance: “is-a” relationship, rigid, tight coupling
  • Example: “A car has an engine (composition), but a sedan is a car (inheritance)”
  • Conclude with: “I prefer composition when possible because it’s more flexible and easier to test”

2. Draw the Diagram

When asked about composition, offer to draw a UML diagram. Interviewers love this.

What to draw:

  • Two boxes (classes)
  • Filled diamond on the container side
  • Line connecting them
  • Label: “owns” or “1 to many”

Say: “The filled diamond shows composition — the container owns the parts and they can’t exist independently.”


3. Know the “Favor Composition Over Inheritance” Principle

You might be asked: “When should you use composition instead of inheritance?”

Answer with specifics:

  • “When the relationship is ‘has-a’ not ‘is-a’”
  • “When you need flexibility to change behavior at runtime”
  • “When you want to avoid deep inheritance hierarchies”
  • “When you need to combine behaviors from multiple sources”

Example: “Instead of inheriting from multiple classes (which Python allows but gets messy), I’d compose objects with different behaviors.”


4. Discuss Lifecycle Management

Advanced question: “How do you handle memory management with composition?”

Python answer: “Python’s garbage collector handles it automatically. When the container is destroyed and no other references exist, composed objects are cleaned up.”

C++ answer: “In C++, I use RAII — composed objects are destroyed automatically when the container’s destructor runs. For dynamic allocation, I use smart pointers like unique_ptr to ensure proper cleanup.”

Java answer: “Java’s garbage collector handles it, but I’m careful about resource cleanup. For resources like file handles, I implement AutoCloseable and use try-with-resources.”


5. Provide Real-World Design Examples

Be prepared to design a system using composition:

Example prompt: “Design a Library system.”

Strong approach:

  • Identify composition relationships: “A Library owns Books, a Book owns Pages”
  • Identify aggregation: “A Library has Members, but Members exist independently”
  • Explain: “I’d use composition for Books and Pages because they’re tightly coupled. A Page doesn’t make sense without its Book.”

6. Code It Live

If asked to implement composition:

  • Start with the simplest case (one container, one part)
  • Create parts in the constructor
  • Show how destruction works
  • Add a method that uses the composed objects

Template:

class Container:
    def __init__(self):
        self.part = Part()  # Create part here
    
    def use_part(self):
        return self.part.do_something()

7. Mention Testing Benefits

Advanced tip: “Composition makes testing easier because I can mock the composed objects. With inheritance, I’m stuck with the parent class’s behavior.”

This shows you think about maintainability and testing — highly valued in interviews.

Key Takeaways

  • Composition is a strong “owns-a” relationship where the container creates and owns its parts, and parts cannot exist independently. When the container is destroyed, all parts are destroyed.

  • Create parts in the constructor of the container class. This ensures the container controls the entire lifecycle of its parts, which is the defining characteristic of composition.

  • Composition vs. Aggregation vs. Inheritance: Composition is “owns-a” with strong lifecycle dependency, aggregation is “has-a” with independent parts, and inheritance is “is-a” for type relationships.

  • Favor composition over inheritance for flexibility, loose coupling, and easier testing. Use composition when you need “has-a” relationships or want to combine behaviors from multiple sources.

  • In interviews, be ready to: explain the difference between composition and other relationships, draw UML diagrams with filled diamonds, discuss lifecycle management, and design systems using composition appropriately.