ReviseAlgo Logo

Class Relationships

Composition

Has-a relationship (strong)

Last Updated: June 26, 2026 20 min read

Composition is the strongest form of "has-a" relationship in Object-Oriented Design. It represents a strict whole-part ownership structure where the parts cannot survive independently of the whole. When the container (the "whole") is destroyed, all of its contained elements (the "parts") are automatically destroyed alongside it.

1. Learning Objectives

  • Define Composition and identify its UML filled diamond representation.
  • Distinguish Composition from Association and Aggregation based on ownership and lifecycles.
  • Implement strict lifecycle binding by instantiating parts internally within the owner class.
  • Avoid memory leaks in manual memory systems (destructors) and GC systems (reference leaks).
  • Prevent external reference leakage using deep-copying techniques.

2. Problem Statement

In many domain-driven designs, certain entities are entirely dependent on their parent context. For instance, an OrderItem only makes sense within the context of an Order, and a Room has no meaning outside of a House.

If we allow the outer client code to create these child parts and pass them into the container (Aggregation), we expose their references to the outside world. This can lead to anomalies:

  • External actors can modify the internal state of a room bypassing the house's controls.
  • If the house is demolished in the database, the rooms might remain as "orphaned" records, causing data inconsistency.

We need a design pattern that enforces exclusive ownership, restricts direct child instantiation from the outside, and guarantees unified deletion.

3. Real-world Analogy

Think of a House and its Rooms:

  • You cannot purchase a kitchen in a store, take it home, and then attach it to a house. The kitchen is built directly inside the walls of the house during construction.
  • The kitchen belongs exclusively to that house. It cannot be shared simultaneously with your neighbor's house.
  • If the house is demolished, the kitchen is destroyed as well. They share a coincident lifecycle.

This strong, exclusive containment represents Composition.

4. Theory

Composition represents a strong "has-a" relationship. In UML diagrams, it is represented by a filled diamond on the container's side.

Key Characteristics:

  • Exclusive Ownership: A part can belong to at most one whole at a time. It cannot be shared.
  • Coincident Lifecycle: The whole class is responsible for creating and destroying its parts. The parts cannot exist before or outlive the whole.
  • Encapsulated Creation: Parts are instantiated directly inside the constructor or initialization methods of the whole class.
  • No Cascading Leakage: If references to composed parts are exposed, they must be returned as read-only copies or unmodifiable views.

5. Visual Diagrams (UML & Memory structures)

Class Diagram

The filled black diamond indicates Composition. It is placed on the side of the container (House).

House
- address: String
- rooms: List<Room>
+ addRoom(name, area)
1               1..*
Filled Diamond (Composite)
Room
- name: String
- area: double
+ getArea(): double

Object Diagram

Instances at runtime showing total ownership containment:

myHouse: House
address = "123 Main St"
owns parts
livingRoom: Room
area = 400.0
kitchen: Room
area = 200.0

Memory Diagram (Verifying Cascaded Sweep)

When myHouse is set to null, the garbage collector sweeps the container. Since there are no other references to the Room instances on the stack, all composed room objects are swept in the same pass.

Stack Frame

myHouse = @0xBC45
points to

Heap space (Swept together)

@0xBC45 (House): [GC'd]
address: "123 Main St"
rooms: [@0xDE12, @0xDE89]
@0xDE12 (Room): [GC'd]
name: "Living Room"

Object Lifecycle

1. Unified Creation (Outer scope instantiates House; House constructor triggers creation of Rooms)
2. Tied Operation (Client accesses room parameters ONLY through house interface)
3. Cascaded Demolition (Demolishing House sweeps all internal rooms from memory simultaneously)

6. Syntax Explanation

Unlike Aggregation, child objects are instantiated inside the container.

  • Java: Constructors of part classes are often package-private or inner classes. The parent instantiates them inside: rooms.add(new Room(name, area));.
  • Python: Instantiation occurs inside the initializer or helper methods: self._rooms.append(Room(name, area)).
  • C++: Can be implemented by nesting values directly inside vectors: std::vector<Room> rooms; or using exclusive smart pointer scopes: std::vector<std::unique_ptr<Room>> rooms;. When the parent object is destroyed, the vector's elements are automatically cleaned up.

7. Step-by-Step Implementation

  • Step 1: Create the Room class. Make its constructor package-private or restricted so that external code cannot instantiate a Room independently.
  • Step 2: Create the House class. Declare a private collection to hold Room objects.
  • Step 3: Inside the House constructor, call new Room(...) to instantiate the parts. Implement an addRoom(String name, double area) method that creates and appends new rooms.
  • Step 4: Write a verification client to confirm that clearing the House reference leaves no accessible reference path to the inner rooms.

8. Complete Code (Mini Project)

9. Code Walkthrough

Let's look at how lifecycle binding is enforced in the code:

  • Internal Creation: The constructor of House directly instantiates the rooms. We do not pass existing Room references into the constructor.
  • Access Restriction: In the Java code, the constructor of Room is package-private (no access modifier). This prevents classes outside of the com.revisealgo.lld package from instantiating Room directly.
  • Exclusive Ownership in C++: Using std::vector<std::unique_ptr<Room>> ensures that only the House owns the pointers to the rooms. A unique_ptr cannot be copied, preventing other classes from sharing ownership of the same room. When the House destructor is called, the unique_ptrs are cleaned up, releasing the rooms from the heap.

10. Execution Flow

  1. Construction: Client constructs myHouse. The constructor immediately instantiates default Room objects inside the parent layout.
  2. Operation: Client interacts with the house, querying metrics like total area. The client does not reference or modify room objects directly.
  3. Destruction: The myHouse stack reference is cleared or goes out of scope:
    • In C++, the destructor deallocates the rooms from the heap.
    • In Java/Python, the garbage collector sweeps the rooms because there are no remaining reference paths from the stack.

11. Internal Working

At runtime, the memory footprint of composed objects is fully contained within the container's hierarchy:

  • In C++, if we declare fields directly (e.g. Room livingRoom;), the room data is allocated inside the parent's memory layout. Demolishing the parent cleans up the entire memory block in a single step.
  • In Java/Python, when myHouse = null is executed, the reference pointer pointing to the parent object on the heap is severed. The garbage collector checks for other references to the parent or its children. Because the child objects were created internally and never exposed, they have a reference count of zero and are cleaned up.

12. Complexity Analysis

  • Time Complexity:
    • Instantiation: $O(C)$ to construct the container and its $C$ composed parts.
    • Destruction: $O(C)$ to clean up all parts in manual memory systems.
  • Space Complexity: $O(C)$ to store the data for all composed parts on the heap.

13. Best Practices

  • Create parts internally: Instantiate all composed objects inside the parent class's constructor or initialization methods.
  • Restrict constructor access: Use package-private (Java) or private namespaces (C++) to prevent client code from instantiating parts independently.
  • Do not leak references: If a client needs to inspect composed parts, return read-only views, deep copies, or primitive summaries rather than raw references.
  • Prefer composition over inheritance: Composition provides a more flexible design than inheritance by letting you swap behaviors at runtime without coupling class hierarchies.

14. Common Mistakes

  • Leaking references via getters: Returning raw pointers to composed objects (e.g. return rooms;). This allows client code to modify internal lists and bypass the parent's business rules.
  • Allowing external instantiation: Leaving the constructor of the part class public, allowing developers to bypass the container class entirely.
  • Memory leaks in C++: Using raw pointers (Room*) inside the container class and forgetting to write a destructor that calls delete, leaking the parts when the container goes out of scope. Use smart pointers like unique_ptr to automate cleanup.

15. Interview Questions

Q: What is the main difference in lifecycle management between Aggregation and Composition?
Answer: In Aggregation, the lifecycle of the parts is independent of the whole. Parts are created outside and survive the destruction of the whole. In Composition, the lifecycles are tied. The whole creates the parts and destroys them when it is deleted.
Q: How do you implement Composition in C++ to guarantee automatic cleanup?
Answer: Use std::unique_ptr inside a vector or declare the child instances as direct member values of the parent class. Both approaches ensure that the parts are automatically cleaned up when the parent object is destroyed.
Q: What does the design guideline "Prefer Composition over Inheritance" mean?
Answer: It means you should build complex behaviors by combining small, focused objects (Composition) rather than building deep class inheritance hierarchies. Composition makes the system more modular, easier to test, and flexible to change at runtime.

16. Practice Exercises

  • Easy: Implement a Car and Engine model where the engine is created inside the car's constructor and cannot exist independently.
  • Medium: Design an Order and OrderItem relationship. Ensure that order items can only be added by passing parameters to order.addItem(itemId, price, quantity), preventing direct instantiation of items in the client code.
  • Hard: Build a Document editor where a document owns a list of Paragraph objects, and each paragraph owns a list of Line objects. Write a deep copy constructor for Document that duplicates the entire document hierarchy.

17. Challenge Problem

Design a transaction-safe file system simulator where a Folder owns a list of composed Files. Implement a deep copy constructor for Folder that duplicates the entire folder and file tree structures in memory without sharing any file references.

18. Summary

  • Composition represents a strong whole-part relationship where the parts cannot survive without the whole.
  • Parts are created directly inside the parent container to enforce coincident lifecycles.
  • In UML, Composition is represented by a filled black diamond pointing to the owner class.
  • Exposing references to composed parts should be avoided; return deep copies or unmodifiable views instead.

19. Cheat Sheet

Relationship Type Ownership Strength UML Notation Lifecycle Constraint
Association None (Peers) Solid line (or arrow) Fully Independent
Aggregation Weak whole-part Hollow diamond pointing to whole Independent survival
Composition Strong whole-part Filled diamond pointing to whole Part dies with whole

20. Quiz

1. What is the defining characteristic of Composition?

A) The parts can belong to multiple container instances
B) The parts are created externally and survive container deletion
C) The parts share a coincident lifecycle with the whole and cannot exist independently (Correct)

2. Which UML symbol represents Composition?

A) Hollow diamond pointing to whole
B) Solid filled diamond pointing to whole (Correct)
C) Dotted arrow pointing to part

3. Where should composed child objects be instantiated?

A) In a static utility class registry
B) Directly inside the constructor or initialization methods of the whole class (Correct)
C) In the client code, then passed via setters

4. What is a key design risk when exposing raw getter lists to composed elements?

A) It decreases thread safety speeds
B) It leaks references, allowing clients to bypass the container class and modify internal state (Correct)
C) It causes class inheritance compilation errors

5. In Java, how can we restrict client code from instantiating composed classes directly?

A) Use package-private or private constructors on the composed classes (Correct)
B) Mark the composed class as final
C) Make the container class abstract

6. What type of smart pointer is ideal to represent Composition in C++?

A) std::shared_ptr
B) std::unique_ptr (Correct)
C) std::weak_ptr

7. Why is "Prefer Composition over Inheritance" recommended in OOP design?

A) It yields flatter class structures that are more modular and flexible to change (Correct)
B) It speeds up compilation execution times
C) It forces all variables to reside on stack frames

8. What happens to composed order items when an order is deleted, if the database schema is configured correctly?

A) They are kept as orphaned records
B) They are deleted automatically via a cascade delete constraint (Correct)
C) They are reassigned to a default parent order

9. What complexity is associated with copying a composite object?

A) Shallow copies are sufficient
B) A deep copy is required to clone all composed parts and prevent reference sharing (Correct)
C) Space complexity is O(1)

10. What is a common mistake when implementing Composition in C++?

A) Failing to delete raw pointers inside the destructor, causing memory leaks (Correct)
B) Using value types inside container arrays
C) Declaring the container as final

21. Next Lesson Preview

In the next lesson, we will explore Dependency to see how classes can utilize other short-lived objects as method parameters without holding them as permanent attributes!