Composite Design Pattern


Problem 

Software often needs to treat individual objects and nested groups of objects uniformly. File systems contain files and directories, drawing tools contain primitive shapes and grouped drawings, and menu systems contain both single menu items and complete submenus. If a client has to distinguish between every leaf and every container, the code quickly fills with special cases and repeated tree traversal logic.

A classic motivating example is a graphics editor: it works with primitives like Line, Rectangle, and Text, but it also supports Picture objects that group these primitives (and other pictures) into composite drawings. Clients want to call draw() on either a primitive or a picture without checking which kind of object they are holding.

Context

The Composite pattern applies when the domain is naturally recursive: a whole is built from parts, and some parts can themselves contain further parts. In such systems, clients want one common abstraction for both single objects and containers so they can issue operations like print(), render(), or totalPrice() without checking whether the receiver is a leaf or a branch.

Intent

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

Solution

The Composite Pattern introduces a common Component abstraction shared by both atomic elements (Leaf) and containers (Composite). The composite stores child components and forwards operations recursively to them. Clients program only against the Component interface, which keeps the traversal logic inside the structure rather than scattering it across the application.

Participants

  • Component (e.g., Graphic, MenuComponent): declares the interface for objects in the composition; implements default behavior for the interface common to all classes; declares an interface for accessing and managing its child components; optionally defines an interface for accessing a component’s parent.
  • Leaf (e.g., Rectangle, Line, Text, MenuItem): represents leaf objects in the composition. A leaf has no children and defines behavior for primitive objects.
  • Composite (e.g., Picture, Menu): defines behavior for components having children; stores child components; implements child-related operations in the Component interface.
  • Client: manipulates objects in the composition through the Component interface.

UML Role Diagram

UML Example Diagram

Sequence Diagram

Design Decisions

Transparent vs. Safe Composite

This is the fundamental design trade-off of the Composite pattern:

  • Transparent composite: The full child-management interface (add(), remove(), getChild()) is declared on Component, so clients can treat leaves and composites identically through a single interface. This maximizes uniformity but means leaves inherit methods that make no sense for them (e.g., add() on a MenuItem). Leaves must either throw an exception or silently ignore these calls.

  • Safe composite: Only Composite exposes add() and remove(), preventing nonsensical operations on leaves at compile time. But clients must now distinguish between leaves and composites when managing children, reducing the pattern’s primary benefit of uniform treatment.

Neither approach is universally better—the choice depends on whether uniformity (transparent) or type safety (safe) is more important in your context.

Child Ownership

If child objects cannot exist independently of their parent, use composition semantics and let the composite own the child lifetime. If children may be shared across multiple structures, model a weaker association instead. In UML, this distinction maps to filled-diamond composition vs. open-diamond aggregation.

Parent References

Adding a parent reference to Component enables upward traversal (e.g., “which menu does this item belong to?”) but complicates add() and remove() operations, which must now maintain bidirectional consistency. The usual place to define the parent reference is in the Component class so leaves and composites can inherit it. The invariant to maintain is that all children of a composite have that composite as their parent — the simplest way to enforce this is to set the parent only inside the composite’s add() and remove().

Sharing Components

Sharing components is useful for reducing storage requirements, but a component with a single parent reference cannot be shared across multiple composites. One option is to let children store multiple parents; another is to drop parent references altogether and externalize the relevant state, which is the approach taken by the Flyweight pattern.

Child Storage and Ordering

Several smaller decisions arise once you commit to a Composite design:

  • Where to store the children: Putting the child collection in the Component base class is convenient but pays a per-leaf storage cost for a list that leaves never use. It is only worthwhile when there are relatively few leaves in the structure.
  • Child ordering: Many domains require an ordering on children (front-to-back rendering, the order of statements in a parse tree, the order of items on a menu). Design add(), remove(), and traversal carefully when order matters; an explicit Iterator often pays for itself here.
  • Caching: A composite that is traversed or searched frequently can cache aggregated information about its children (e.g., a bounding box of all child shapes). Any change to a child must invalidate the caches of its ancestors, which is easiest to coordinate when components hold parent references.
  • Choice of data structure: There is no single right collection — linked lists, arrays, hash tables, even per-child fields are all reasonable depending on access patterns and child count.

Consequences

  • Defines class hierarchies of primitive and composite objects. Primitive objects can be composed into more complex objects, which in turn can be composed recursively. Wherever client code expects a primitive object, it can also accept a composite.
  • Makes the client simple. Clients can treat composite structures and individual objects uniformly and need not write tag-and-case-statement-style logic over the classes that define the composition.
  • Makes it easier to add new kinds of components. New Composite or Leaf subclasses work automatically with existing structures and existing client code.
  • Can make your design overly general. It becomes harder to restrict which components a composite may contain. The type system cannot enforce “only these kinds of children are allowed”; you must fall back on run-time checks.

Composite in Pattern Compounds

The Composite pattern frequently appears as a building block in larger pattern compounds, because many patterns need to operate on tree structures:

  • Composite + Builder: The Builder pattern can construct complex Composite structures step by step. The Composite’s Component acts as the Builder’s product, and the Builder handles the complexity of assembling the recursive tree.
  • Composite + Visitor: When many distinct operations need to be performed on a Composite structure without modifying its classes, the Visitor pattern provides a clean separation of concerns. This is especially useful when new operations are added frequently but new leaf types are rare.
  • Composite + Iterator: An Iterator can traverse the Composite tree in different orders (depth-first, breadth-first) without exposing the tree’s internal structure to the client.
  • Composite + Command: A Composite Command groups multiple command objects into a tree, allowing hierarchical undo/redo operations and macro commands that execute sub-commands in sequence.

These compounds are so common that recognizing the Composite pattern is often the first step toward identifying a larger architectural pattern at work.

Code Example

This example uses a transparent composite: both Menu and MenuItem share the same print() operation, while only composite menus do real work in add().

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

import java.util.ArrayList;
import java.util.List;

abstract class MenuComponent {
    void add(MenuComponent component) {
        throw new UnsupportedOperationException("leaf cannot contain children");
    }

    abstract void print();
}

final class MenuItem extends MenuComponent {
    private final String name;

    MenuItem(String name) {
        this.name = name;
    }

    void print() {
        System.out.println(name);
    }
}

final class Menu extends MenuComponent {
    private final String name;
    private final List<MenuComponent> children = new ArrayList<>();

    Menu(String name) {
        this.name = name;
    }

    void add(MenuComponent component) {
        children.add(component);
    }

    void print() {
        System.out.println("\n" + name);
        children.forEach(MenuComponent::print);
    }
}

public class Demo {
    public static void main(String[] args) {
        Menu allMenus = new Menu("All Menus");
        Menu dessert = new Menu("Dessert Menu");
        dessert.add(new MenuItem("Apple pie"));
        allMenus.add(new MenuItem("Pancakes"));
        allMenus.add(dessert);
        allMenus.print();
    }
}
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

class MenuComponent {
public:
    virtual ~MenuComponent() = default;
    virtual void add(std::unique_ptr<MenuComponent>) {
        throw std::logic_error("leaf cannot contain children");
    }
    virtual void print() const = 0;
};

class MenuItem : public MenuComponent {
public:
    explicit MenuItem(std::string name) : name_(std::move(name)) {}

    void print() const override {
        std::cout << name_ << "\n";
    }

private:
    std::string name_;
};

class Menu : public MenuComponent {
public:
    explicit Menu(std::string name) : name_(std::move(name)) {}

    void add(std::unique_ptr<MenuComponent> component) override {
        children_.push_back(std::move(component));
    }

    void print() const override {
        std::cout << "\n" << name_ << "\n";
        for (const auto& child : children_) {
            child->print();
        }
    }

private:
    std::string name_;
    std::vector<std::unique_ptr<MenuComponent>> children_;
};

int main() {
    auto allMenus = std::make_unique<Menu>("All Menus");
    auto dessert = std::make_unique<Menu>("Dessert Menu");
    dessert->add(std::make_unique<MenuItem>("Apple pie"));
    allMenus->add(std::make_unique<MenuItem>("Pancakes"));
    allMenus->add(std::move(dessert));
    allMenus->print();
}
from abc import ABC, abstractmethod


class MenuComponent(ABC):
    def add(self, component: "MenuComponent") -> None:
        raise NotImplementedError("leaf cannot contain children")

    @abstractmethod
    def print(self) -> None:
        pass


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

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


class Menu(MenuComponent):
    def __init__(self, name: str) -> None:
        self.name = name
        self.children: list[MenuComponent] = []

    def add(self, component: MenuComponent) -> None:
        self.children.append(component)

    def print(self) -> None:
        print(f"\n{self.name}")
        for child in self.children:
            child.print()


all_menus = Menu("All Menus")
dessert = Menu("Dessert Menu")
dessert.add(MenuItem("Apple pie"))
all_menus.add(MenuItem("Pancakes"))
all_menus.add(dessert)
all_menus.print()
abstract class MenuComponent {
  add(component: MenuComponent): void {
    throw new Error("leaf cannot contain children");
  }

  abstract print(): void;
}

class MenuItem extends MenuComponent {
  constructor(private readonly name: string) {
    super();
  }

  print(): void {
    console.log(this.name);
  }
}

class Menu extends MenuComponent {
  private readonly children: MenuComponent[] = [];

  constructor(private readonly name: string) {
    super();
  }

  add(component: MenuComponent): void {
    this.children.push(component);
  }

  print(): void {
    console.log(`\n${this.name}`);
    this.children.forEach((child) => child.print());
  }
}

const allMenus = new Menu("All Menus");
const dessert = new Menu("Dessert Menu");
dessert.add(new MenuItem("Apple pie"));
allMenus.add(new MenuItem("Pancakes"));
allMenus.add(dessert);
allMenus.print();

Flashcards

Structural Pattern Flashcards

Key concepts for Adapter, Composite, and Facade patterns.

Difficulty: Basic

What problem does Adapter solve?

Difficulty: Intermediate

Object Adapter vs. Class Adapter?

Difficulty: Intermediate

Adapter vs. Facade vs. Decorator?

Difficulty: Advanced

What does POSA5 say about ‘the Adapter pattern’?

Difficulty: Basic

What problem does Composite solve?

Difficulty: Advanced

Composite: Transparent vs. Safe design?

Difficulty: Advanced

Name three pattern compounds involving Composite.

Difficulty: Basic

What problem does Facade solve?

Difficulty: Advanced

Facade vs. Mediator: what’s the communication direction?

Difficulty: Intermediate

Should the subsystem know about its Facade?

Quiz

Structural Patterns Quiz

Test your understanding of Adapter, Composite, and Facade — their distinctions, design decisions, and when to apply each.

Difficulty: Advanced

A TurkeyAdapter implements the Duck interface. The fly() method calls turkey.fly() five times in a loop because a duck’s flight is much longer than a turkey’s short hop. What design concern does this raise?

Correct Answer:
Difficulty: Intermediate

A colleague says: “We should use an Adapter between our service and the database layer.” Your team wrote both the service and the database layer. What is the best response?

Correct Answer:
Difficulty: Intermediate

In a Composite pattern for a restaurant menu system, a developer declares add(MenuComponent) on the abstract MenuComponent class (inherited by both Menu and MenuItem). A tester calls menuItem.add(anotherItem). What happens, and what design trade-off does this illustrate?

Correct Answer:
Difficulty: Advanced

All three patterns — Adapter, Facade, and Decorator — involve “wrapping” another object. What is the key distinction between them?

Correct Answer:
Difficulty: Advanced

A HomeTheaterFacade exposes watchMovie(), endMovie(), listenToMusic(), stopMusic(), playGame(), setupKaraoke(), and calibrateSystem(). The class is growing difficult to maintain. What is the best architectural response?

Correct Answer:
Difficulty: Advanced

The Facade’s communication is one-directional: the Facade calls subsystem classes, but the subsystem does not know about the Facade. The Mediator’s communication is bidirectional. Why does this distinction matter architecturally?

Correct Answer: