Skip to main content

Composition — Has-A with Dependent Lifecycle

Editorial

Introduction

A University has Departments. Not just any departments — departments that were created by the university, exist because of the university, and will cease to exist if the university shuts down. The "Computer Science Department" at MIT is not a free-floating entity that was assigned to MIT — it's an intrinsic part of MIT. If MIT closes, the MIT CS Department no longer exists.

This is Composition: the strongest form of "has-a" relationship. The whole owns its parts, creates them, and controls their lifecycle. When the whole dies, the parts die with it.

In the previous tutorial, we saw Aggregation — where a Department has Professors but doesn't control their existence. Composition tightens this: the whole owns the parts exclusively, and their lives are bound together.

Composition is one of the most powerful concepts in object-oriented design. The famous principle "favor composition over inheritance" (covered later in this section) is built entirely on this relationship. Understanding it deeply is prerequisite to mastering design patterns in S10–S12.

What Makes Composition Different from Aggregation?

The critical difference comes down to lifecycle dependency and exclusive ownership:

Aggregation (◇ open diamond): "I have parts, but they can exist without me."

  • Department ◇→ Professor: professors survive department dissolution
  • Parts are created externally and passed in
  • Parts can be shared across multiple wholes

Composition (◆ filled diamond): "I own parts that cannot exist without me."

  • University ◆→ Department: departments are destroyed when the university closes
  • Parts are created by the whole (or at least for the whole)
  • Parts belong to exactly ONE whole — no sharing

Real-world examples that clarify the distinction:

  • A house has rooms → Composition. If the house is demolished, the rooms cease to exist. A room doesn't float to another house.
  • A car has an engine → Composition. The engine was built as part of this car. If the car is scrapped, the engine goes with it.
  • An order has order items → Composition. "Item #3 of Order #5001" has no meaning without Order #5001.
  • A human body has a heart → Composition. The heart doesn't exist independently.

Contrast with Aggregation:

  • A parking lot has cars → Aggregation. Cars drive away independently.
  • A team has players → Aggregation. Players join other teams.

UML Notation

Composition in UML, the strongest has-a relationship: the whole creates its parts, owns them exclusively and controls their lifecycle. UML notation: a filled diamond on the whole's side, with an arrow to the part — University ◆→ Department and Order ◆→ OrderItem, each labeled "owns", with multiplicity 1 at the whole and * at the part. The code tell, from the lesson: the aggregation pattern passes parts in from outside (dept = Department("CS", 500000); prof = Professor("P001", "Smith", "Algorithms"); dept.add_professor(prof)), while the composition pattern creates parts by the whole (university = University("MIT"); university.create_department("CS", 500000)). Aggregation vs composition: lifecycle — parts survive the whole versus die with it; ownership — can be shared by many wholes versus exactly one; creation — created outside and passed in versus created by (or for) the whole. Real-world compositions drawn the same way: House ◆→ Room, Car ◆→ Engine, Order ◆→ OrderItem, Body ◆→ Heart. Test: destroy the whole — are its parts destroyed too? Yes means composition; in databases it maps to ON DELETE CASCADE.

Visualization

The University creates its departments internally (composition, not passed in). Each part is owned exclusively - trying to share one with another university is blocked. Shut the university down and every department is destroyed with it (cascade). Contrast this with aggregation, where parts are pooled, shared, and survive.

Code Example

Two examples demonstrate Composition clearly:

  1. University → Department: departments are created by the university and destroyed with it
  2. Order → OrderItem: order items are created within an order and have no independent existence

Notice the key code pattern: the whole creates the parts internally (unlike Aggregation, where parts are passed in from outside).

from typing import List


class Department:
    """A department that exists only as part of a University."""

    def __init__(self, name: str, budget: float):
        self._name = name
        self._budget = budget
        self._is_active = True

    @property
    def name(self) -> str:
        return self._name

    @property
    def is_active(self) -> bool:
        return self._is_active

    def deactivate(self) -> None:
        """Called when the university destroys this department."""
        self._is_active = False
        print(f"    {self._name} department deactivated")

    def __repr__(self) -> str:
        status = "active" if self._is_active else "INACTIVE"
        return f"{self._name} [{status}]"


class University:
    """
    University COMPOSES departments.
    Key differences from Aggregation:
    1. Departments are CREATED by the university (not passed in)
    2. Departments are DESTROYED when the university shuts down
    3. Departments belong to exactly ONE university
    """

    def __init__(self, name: str):
        self._name = name
        self._departments: List[Department] = []

    def create_department(self, dept_name: str, budget: float) -> Department:
        """University CREATES the department — composition pattern."""
        dept = Department(dept_name, budget)  # Created BY the whole
        self._departments.append(dept)
        print(f"  {self._name} created {dept_name} department")
        return dept

    def shutdown(self) -> None:
        """Shut down the university — ALL departments are destroyed."""
        print(f"\n  {self._name} is shutting down...")
        for dept in self._departments:
            dept.deactivate()  # Parts destroyed with the whole
        self._departments.clear()
        print(f"  {self._name} shutdown complete. All departments gone.")

    def __repr__(self) -> str:
        dept_names = [d.name for d in self._departments]
        return f"{self._name} University (departments: {dept_names})"


# --- Order/OrderItem example ---
class OrderItem:
    """An item that exists only within an Order."""

    def __init__(self, product_name: str, quantity: int, unit_price: float):
        self._product_name = product_name
        self._quantity = quantity
        self._unit_price = unit_price

    def get_subtotal(self) -> float:
        return self._quantity * self._unit_price

    def __repr__(self) -> str:
        return f"{self._product_name} x{self._quantity} @ ${self._unit_price:.2f}"


class Order:
    """Order COMPOSES OrderItems — items are created BY and destroyed WITH the order."""

    def __init__(self, order_id: str):
        self._order_id = order_id
        self._items: List[OrderItem] = []
        self._status = "ACTIVE"

    def add_item(self, product_name: str, quantity: int, unit_price: float) -> None:
        """Order CREATES the item internally — composition pattern."""
        item = OrderItem(product_name, quantity, unit_price)  # Created BY the whole
        self._items.append(item)
        print(f"  Added {item} to order {self._order_id}")

    def get_total(self) -> float:
        return sum(item.get_subtotal() for item in self._items)

    def cancel(self) -> None:
        """Cancel order — ALL items are destroyed."""
        print(f"  Cancelling order {self._order_id}...")
        self._items.clear()  # Items destroyed with the order
        self._status = "CANCELLED"
        print(f"  Order {self._order_id} cancelled. All items removed.")

    def __repr__(self) -> str:
        return f"Order {self._order_id} [{self._status}] ({len(self._items)} items, total: ${self.get_total():.2f})"


# Demonstration
print("=== University Composition ===")
mit = University("MIT")
mit.create_department("Computer Science", 500_000)
mit.create_department("Mathematics", 300_000)
mit.create_department("Physics", 400_000)
print(f"  {mit}")

mit.shutdown()  # All departments destroyed

print("\n=== Order Composition ===")
order = Order("ORD-5001")
order.add_item("Laptop", 1, 999.99)
order.add_item("Mouse", 2, 29.99)
order.add_item("USB Cable", 3, 9.99)
print(f"  {order}")

order.cancel()  # All items destroyed
print(f"  {order}")
import java.util.ArrayList;
import java.util.List;

class Department {
    private final String name;
    private final double budget;
    private boolean active = true;

    public Department(String name, double budget) {
        this.name = name;
        this.budget = budget;
    }

    public String getName() {
        return name;
    }

    public void deactivate() {
        active = false;
        System.out.println("    " + name + " department deactivated");
    }

    @Override
    public String toString() {
        return name + " [" + (active ? "active" : "INACTIVE") + "]";
    }
}

class University {
    private final String name;
    private final List<Department> departments = new ArrayList<>();

    public University(String name) {
        this.name = name;
    }

    // University CREATES the department — composition pattern
    public Department createDepartment(String deptName, double budget) {
        Department dept = new Department(deptName, budget); // Created BY the whole
        departments.add(dept);
        System.out.println("  " + name + " created " + deptName + " department");
        return dept;
    }

    // Shutdown destroys ALL departments
    public void shutdown() {
        System.out.println("\n  " + name + " is shutting down...");
        for (Department dept : departments) {
            dept.deactivate(); // Parts destroyed with the whole
        }
        departments.clear();
        System.out.println("  " + name + " shutdown complete. All departments gone.");
    }

    @Override
    public String toString() {
        List<String> names = departments.stream().map(Department::getName).toList();
        return name + " University (departments: " + names + ")";
    }
}

class OrderItem {
    private final String productName;
    private final int quantity;
    private final double unitPrice;

    public OrderItem(String productName, int quantity, double unitPrice) {
        this.productName = productName;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    public double getSubtotal() {
        return quantity * unitPrice;
    }

    @Override
    public String toString() {
        return productName + " x" + quantity + " @ $" + String.format("%.2f", unitPrice);
    }
}

class Order {
    private final String orderId;
    private final List<OrderItem> items = new ArrayList<>();
    private String status = "ACTIVE";

    public Order(String orderId) {
        this.orderId = orderId;
    }

    // Order CREATES the item — composition pattern
    public void addItem(String productName, int quantity, double unitPrice) {
        OrderItem item = new OrderItem(productName, quantity, unitPrice);
        items.add(item);
        System.out.println("  Added " + item + " to order " + orderId);
    }

    public double getTotal() {
        return items.stream().mapToDouble(OrderItem::getSubtotal).sum();
    }

    public void cancel() {
        System.out.println("  Cancelling order " + orderId + "...");
        items.clear(); // Items destroyed with the order
        status = "CANCELLED";
        System.out.println("  Order " + orderId + " cancelled. All items removed.");
    }

    @Override
    public String toString() {
        return "Order " + orderId + " [" + status + "] (" + items.size() + " items, total: $" + String.format("%.2f", getTotal()) + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== University Composition ===");
        University mit = new University("MIT");
        mit.createDepartment("Computer Science", 500_000);
        mit.createDepartment("Mathematics", 300_000);
        mit.createDepartment("Physics", 400_000);
        System.out.println("  " + mit);

        mit.shutdown(); // All departments destroyed

        System.out.println("\n=== Order Composition ===");
        Order order = new Order("ORD-5001");
        order.addItem("Laptop", 1, 999.99);
        order.addItem("Mouse", 2, 29.99);
        order.addItem("USB Cable", 3, 9.99);
        System.out.println("  " + order);

        order.cancel(); // All items destroyed
        System.out.println("  " + order);
    }
}

Composition vs Aggregation — The Code Pattern Difference

There's a recognizable pattern in code that distinguishes Composition from Aggregation:

Aggregation pattern — parts passed IN from outside:

dept = Department("CS", 500000)
prof = Professor("P001", "Smith", "Algorithms")  # Created independently
dept.add_professor(prof)  # Passed INTO the department

Composition pattern — parts created BY the whole:

university = University("MIT")
university.create_department("CS", 500000)  # Created BY the university

In Aggregation, the caller creates the part and passes it in. In Composition, the whole creates the part internally. This is a reliable heuristic for identifying which relationship you're looking at in code.

Another code signal: Look at what happens in the destructor or cleanup method:

  • Aggregation: the whole clears its references but doesn't destroy parts
  • Composition: the whole actively destroys/deactivates its parts

Common Mistakes

Mistake 1: Confusing Composition with "Has-a" in General

The problem: Treating every "has-a" relationship as Composition. "A car has a driver" — is that Composition? No. The driver exists independently of the car.

The rule: Composition requires lifecycle dependency. The part must be meaningless or non-existent without the whole. "A car has an engine" → Composition (engine is integral). "A car has a driver" → Association or Aggregation.

Mistake 2: Using Composition When Parts Should Be Shared

The problem: Modeling Department ◆→ Professor as Composition. But professors can belong to multiple departments (joint appointments), and they survive department restructuring.

The fix: If parts can be shared or survive the whole's destruction → use Aggregation (◇), not Composition (◆).

Mistake 3: Not Cleaning Up Parts When the Whole is Destroyed

The problem: You model Composition in UML but don't enforce it in code. The Order is deleted but OrderItems linger in the database as orphans.

The fix: Composition is a design contract. If you use ◆ in UML, your code must cascade deletion. In databases, this maps to ON DELETE CASCADE foreign key constraints.

Key Takeaways

  • Composition is the strongest "has-a" relationship — the whole owns its parts and controls their lifecycle
  • When the whole is destroyed, all parts are destroyed too — this is the defining characteristic
  • Parts are typically created by the whole (not passed in from outside)
  • Parts belong to exactly one whole — no shared membership
  • UML notation: filled (solid) diamond ◆ on the whole side
  • The code pattern: whole creates parts internally (university.create_department()), not externally passed in
  • Composition is the foundation of the "favor composition over inheritance" principle (covered later in S4)
  • In database design, Composition maps to ON DELETE CASCADE — reinforcing lifecycle dependency
  • You'll use Composition extensively in design problems: Order ◆→ OrderItem, House ◆→ Room, Car ◆→ Engine