bigonotes
CS ConceptsDesign Patterns

Composite Pattern

Build tree structures where single items and groups share one interface, so client code treats them the same.

4 min readLast updated Design Patterns

Files and folders, menu items and submenus, employees and departments: many domains are trees where a group can contain items and other groups. Composite gives the item and the group the same interface, so your code can call delete() or print() without asking "is this one thing or many?".

Intent#

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly. (GoF)

Analogy: a file system. Deleting a file deletes it. Deleting a folder deletes it and everything inside. Either way, you just say "delete".

The problem: type checks everywhere#

Without a shared interface, every operation has to check what kind of node it holds:

class MenuItem:
    def __init__(self, name: str):
        self.name = name


class MenuSection:
    def __init__(self):
        self.items: list[MenuItem] = []
        self.subsections: list["MenuSection"] = []


def print_menu(obj: MenuItem | MenuSection) -> None:
    if isinstance(obj, MenuItem):
        print(obj.name)
    elif isinstance(obj, MenuSection):
        for item in obj.items:
            print(item.name)
        for sub in obj.subsections:
            print_menu(sub)
# Every operation repeats this type checking

With Composite:

from abc import ABC, abstractmethod


class MenuComponent(ABC):
    @abstractmethod
    def print(self) -> None: ...


class MenuItem(MenuComponent):
    def __init__(self, name: str):
        self.name = name

    def print(self) -> None:
        print(self.name)


class MenuSection(MenuComponent):
    def __init__(self):
        self._children: list[MenuComponent] = []

    def add(self, child: MenuComponent) -> None:
        self._children.append(child)

    def print(self) -> None:
        for child in self._children:
            child.print()  # item or section, doesn't matter

Structure#

Leaves and groups share one interface, and groups hold children of that interface.

Composite: a Floor contains Rooms and devices; calling turn_on on the floor recurses down to every leaf
Participant Role
Component Common interface for leaves and composites
Leaf An individual object with no children
Composite A container that implements Component and holds a list of Components (leaves or other composites)

The trick is recursive composition: a composite's children are Components, so trees can nest to any depth. A restaurant menu shows it:

  • Menu (composite)
    • Breakfast (composite)
      • Pancakes (leaf)
      • Eggs (leaf)
    • Dinner (composite)
      • Steak (leaf)
      • Desserts (composite)
        • Cake (leaf)
        • Ice cream (leaf)

Example: smart home#

A DeviceGroup is itself a device, so rooms can hold devices and floors can hold rooms:

from abc import ABC, abstractmethod


# Component
class SmartDevice(ABC):
    @abstractmethod
    def turn_on(self) -> None: ...

    @abstractmethod
    def turn_off(self) -> None: ...


# Leaves
class AirConditioner(SmartDevice):
    def turn_on(self) -> None:
        print("AC on")

    def turn_off(self) -> None:
        print("AC off")


class Fan(SmartDevice):
    def turn_on(self) -> None:
        print("Fan on")

    def turn_off(self) -> None:
        print("Fan off")


# Composite: is a SmartDevice AND holds SmartDevices
class DeviceGroup(SmartDevice):
    def __init__(self, name: str):
        self._name = name
        self._devices: list[SmartDevice] = []

    def add(self, device: SmartDevice) -> None:
        self._devices.append(device)

    def remove(self, device: SmartDevice) -> None:
        self._devices.remove(device)

    def turn_on(self) -> None:
        print(f"{self._name}: turning on")
        for device in self._devices:
            device.turn_on()  # recursion happens naturally for a group

    def turn_off(self) -> None:
        for device in self._devices:
            device.turn_off()


if __name__ == "__main__":
    room1 = DeviceGroup("Room 1")
    room1.add(AirConditioner())
    room1.add(Fan())

    floor1 = DeviceGroup("Floor 1")
    floor1.add(room1)  # a group inside a group
    floor1.add(Fan())  # and a leaf at the same level

    floor1.turn_on()   # client doesn't care what's inside

When you call floor1.turn_on():

  1. floor1 loops over its children.
  2. It finds room1 (a composite) and calls room1.turn_on().
  3. room1 loops over its children and calls turn_on() on the AC and the fan.
  4. Back in floor1, the next child (a fan) is turned on directly.

Real-world examples#

Any tree of items and groups is a candidate:

  • GUI toolkits: in Tkinter, a Frame can contain buttons and other Frames.
  • The HTML DOM: elements contain text nodes and other elements.
  • Python's ast module: syntax-tree nodes contain other nodes.
  • General:
    • file systems, org charts, menus
    • graphics editors (shapes and groups)
    • document structure (text inside sections inside chapters)
    • math expressions ((2 + 3) * (4 - 1): numbers are leaves, operators are composites)
  • Business:
    • product catalogs (products inside categories)
    • tasks inside projects, items inside shipping boxes
    • bills of materials (parts inside assemblies)

When to use it#

Composite fits when:

  • Your data is a part-whole hierarchy (a tree).
  • Clients should treat single items and groups the same way.
  • Structures are recursive or change at runtime.
  • You want to get rid of isinstance checks in client code.

When not to use it#

Skip it when:

  • The structure is flat.
  • Leaves and groups need very different operations.
  • You need strict, type-checked rules about what can contain what.
  • It's only one or two levels deep and never nests further.

Pitfalls#

Watch for these traps:

  • Overly general containers. Nothing stops folder.add(PrinterDriver()); add runtime checks if some children aren't allowed.
  • Child-management methods on leaves. If add()/remove() live on Component for uniformity, leaves must throw or ignore them. Putting them only on Composite is safer but less uniform; choose deliberately.
  • Deep recursion on very deep or very large trees can be slow, or hit Python's recursion limit.
  • Cycles. Adding a group to itself (directly or indirectly) causes infinite recursion.

Composite vs similar patterns#

Composite is easy to mix up with these:

Pattern Difference
Decorator Wraps one object in a linear chain; Composite manages many children in a tree
Chain of Responsibility Usually one handler processes a request; in Composite, all children are visited
Flyweight About memory (sharing); Composite is about structure. Leaves of a large tree are often flyweights
Iterator About traversal; often used to walk a Composite

Key takeaways#

  • Composite = tree of objects where leaves and groups share one interface.
  • A composite implements the component interface and holds a list of components.
  • Operations recurse naturally down the tree; clients never type-check.
  • Decide where add/remove live: uniformity vs type safety.

Found a mistake? Tell me on LinkedIn ↗

Further reading#