Composite Pattern: Building Tree Structures with Unified Interfaces

A diagram or illustration at the start is common, but we'll dive straight into the core concept.

What Is the Composite Pattern?

The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It treats individual objects (leaves) and compositions of objects uniformly. The pattern belongs to structural design patterns and establishes a tree-shaped object arrangement.

Key concepts: uniformity, whole-part relationship.

Consider a company's organizational hierarchy: departments contain employees, departments may have sub-departments, and so on. By abstracting both departments and employees into a common Node interface, we ignore whether a node represents a person or a department when traversing or counting.

In Java Swing, containers (Container) can hold other containers or specific componnets like Button or Checkbox. This is another instance of the part-whole approach.

The classic example is file systems: a folder (container) may contain other folders (containers) or files (leaves). Flattening the tree yields a list, but the tree structure explicitly represents hierarchical relationships between nodes and the whole.

Why is this pattern needed? What is its purpose?

The main goal is to provide a uniform interface to clients, even when container and leaf objects have very different properties. We want to abstract their common behaviors to treat them consistently.

Roles in the Composite Pattern

The Composite pattern typically includes three roles:

  • Component (abstract): an interface or abstract class declaring operations common to both leaf and composite objects. It may define default behavior for managing child components.
  • Leaf: represents leaf objects in the composition. A leaf has no children.
  • Composite: container node that can have child components. Its children can be either leaf or other composite nodes.

The crucial point is that the Component interface unifiees all nodes, so the client does not need to know whether a node is a leaf or a composite.

Two Implementation Variants

The Composite pattern has two variations: transparent and safe.

Transparent: the methods for managing children (add, remove) are declared in the abstract Component class. Both leaves and composites expose the same interface, even though some methods may be no-ops for leaves.

Safe: child-management methods are moved to concrete composite classes only, while the abstract Component only declares common operations (like display). Leaves never have add/remove methods.

Transparent Mode

In transparent mode, the abstract class includes all operations. Leaves implement them with empty bodies or throw exceptions.

Abstract Component:

package designpattern.composite;

public abstract class Component {
    String name;

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

    public abstract void add(Component component);
    public abstract void remove(Component component);
    public abstract void show(int depth);
}

Composite:

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

public class Composite extends Component {
    private List<Component> children = new ArrayList<>();

    public Composite(String name) {
        super(name);
    }

    @Override
    public void add(Component component) {
        children.add(component);
    }

    @Override
    public void remove(Component component) {
        children.remove(component);
    }

    @Override
    public void show(int depth) {
        printIndentation(depth);
        System.out.println(name + ":");
        for (Component child : children) {
            child.show(depth + 1);
        }
    }

    private void printIndentation(int depth) {
        for (int i = 0; i < depth; i++) {
            System.out.print("    ");
        }
    }
}

Leaf:

public class Leaf extends Component {

    public Leaf(String name) {
        super(name);
    }

    @Override
    public void add(Component component) {
        // no-op
    }

    @Override
    public void remove(Component component) {
        // no-op
    }

    @Override
    public void show(int depth) {
        for (int i = 0; i < depth; i++) {
            System.out.print("    ");
        }
        System.out.println(name);
    }
}

Client code:

public class Test {
    public static void main(String[] args) {
        Composite folderRoot = new Composite("Memo Folder");
        folderRoot.add(new Leaf("word file"));
        folderRoot.add(new Leaf("ppt file"));

        Composite weekFolder = new Composite("Weekly Report Folder");
        weekFolder.add(new Leaf("20210101 Weekly Report"));
        folderRoot.add(weekFolder);

        Composite noteFolder = new Composite("Notes Folder");
        noteFolder.add(new Leaf("jvm.ppt"));
        noteFolder.add(new Leaf("redis.txt"));
        weekFolder.add(noteFolder);

        folderRoot.add(new Leaf("Requirements.txt"));

        Leaf bugTicket = new Leaf("bug.txt");
        folderRoot.add(bugTicket);
        folderRoot.remove(bugTicket);

        folderRoot.show(0);
    }
}

Output:

Memo Folder:
    word file
    ppt file
    Weekly Report Folder:
        20210101 Weekly Report
        Notes Folder:
            jvm.ppt
            redis.txt
    Requirements.txt

Both leaves and composites are manipulated uniformly, though leaves have empty add/remove methods.

Safe Mode

In safe mode, the abstract component only declares operations common to both types. Add/remove methods are exclusive to composites.

Abstract Component:

public abstract class Component {
    String name;

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

    public abstract void show(int depth);
}

Composite:

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

public class Composite extends Component {
    private List<Component> children = new ArrayList<>();

    public Composite(String name) {
        super(name);
    }

    public void add(Component component) {
        children.add(component);
    }

    public void remove(Component component) {
        children.remove(component);
    }

    @Override
    public void show(int depth) {
        for (int i = 0; i < depth; i++) {
            System.out.print("    ");
        }
        System.out.println(name + ":");
        for (Component child : children) {
            child.show(depth + 1);
        }
    }
}

Leaf:

public class Leaf extends Component {

    public Leaf(String name) {
        super(name);
    }

    @Override
    public void show(int depth) {
        for (int i = 0; i < depth; i++) {
            System.out.print("    ");
        }
        System.out.println(name);
    }
}

With the same client code, the output is identical to the transparent version. Leaves no longer expose add/remove, avoiding empty method calls. However, when processing a collection of Component objects, the client must check if an object is a Composite before calling add/remove.

Summary

Advantages:

  • Allows defining hierarchical objects and handling part-whole relationships uniformly.
  • High-level operations can traverse down to each leaf consistently.
  • Flexible node composition.

Disadvantages:

  • The transparent variant forces leaves to implement child-management methods that do nothing, violating the Interface Segregation Principle.
  • The safe variant requires type checking at runtime when using child-management methods.

Usage scenarios:

  • When you want clients to treat individual objects and compositions uniformly.
  • When representing tree structures of part-whole hierarchies.

As one developer put it:

"Zhang Wuji learned Taiji, forgot all moves, and defeated the 'Two Profound Elders' — the so-called 'no moves in mind.' Design patterns can be seen as moves. If you first master all patterns and then forget them to act spontaneously, that is the highest realm of OO."

Tags: Composite Pattern Design Patterns Software Architecture structural patterns java

Posted on Fri, 04 Sep 2026 16:06:21 +0000 by nerotic