Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 6 min read

Composite Pattern

The Composite pattern lets you compose objects into tree structures and treat individual objects and groups of objects uniformly. A folder can contain files or other folders. A menu can contain items or sub-menus. The client doesn’t need to know whether it’s dealing with a leaf or a branch.

Why this matters: File systems, organizational hierarchies, menus, expression trees, and nested pricing rules all form trees. When the problem says β€œa group can contain other groups,” Composite is the answer.


Prerequisites


Class Diagram

classDiagram
    class FileSystemNode {
        <<interface>>
        +getName() String
        +getSize() long
        +display(indent)
    }
    class File {
        -name: String
        -size: long
        +getName() String
        +getSize() long
        +display(indent)
    }
    class Directory {
        -name: String
        -children: List~FileSystemNode~
        +add(FileSystemNode)
        +remove(FileSystemNode)
        +getName() String
        +getSize() long
        +display(indent)
    }

    FileSystemNode <|.. File
    FileSystemNode <|.. Directory
    Directory o-- FileSystemNode

The key insight: Directory both implements FileSystemNode and contains a list of FileSystemNode. This recursion is what makes the tree structure work.


Real-World Example: File System

// Component interface
public interface FileSystemNode {
    String getName();
    long getSize();
    void display(String indent);
    String getPath();
}

// Leaf: a file has no children
public class File implements FileSystemNode {
    private final String name;
    private final long size;
    private final String extension;

    public File(String name, long size) {
        this.name = name;
        this.size = size;
        this.extension = name.contains(".") ? name.substring(name.lastIndexOf('.')) : "";
    }

    @Override
    public String getName() { return name; }
    
    @Override
    public long getSize() { return size; }
    
    @Override
    public void display(String indent) {
        System.out.println(indent + name + " (" + formatSize(size) + ")");
    }
    
    @Override
    public String getPath() { return name; }
}

// Composite: a directory contains other nodes (files or directories)
public class Directory implements FileSystemNode {
    private final String name;
    private final List<FileSystemNode> children = new ArrayList<>();

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

    public void add(FileSystemNode node) {
        children.add(node);
    }

    public void remove(FileSystemNode node) {
        children.remove(node);
    }

    public List<FileSystemNode> getChildren() {
        return Collections.unmodifiableList(children);
    }

    @Override
    public String getName() { return name; }

    @Override
    public long getSize() {
        // Recursively compute total size
        return children.stream()
            .mapToLong(FileSystemNode::getSize)
            .sum();
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + name + "/");
        for (FileSystemNode child : children) {
            child.display(indent + "  ");
        }
    }
    
    @Override
    public String getPath() { return name + "/"; }

    // Search within the tree
    public List<FileSystemNode> find(String pattern) {
        List<FileSystemNode> results = new ArrayList<>();
        for (FileSystemNode child : children) {
            if (child.getName().matches(pattern)) {
                results.add(child);
            }
            if (child instanceof Directory dir) {
                results.addAll(dir.find(pattern));
            }
        }
        return results;
    }
}

// Usage: client treats files and directories uniformly
Directory root = new Directory("project");
Directory src = new Directory("src");
Directory tests = new Directory("tests");

src.add(new File("Main.java", 2048));
src.add(new File("Utils.java", 1024));
tests.add(new File("MainTest.java", 1536));

root.add(src);
root.add(tests);
root.add(new File("README.md", 512));

// Works the same whether node is File or Directory
System.out.println(root.getSize()); // 5120 (sum of all files)
root.display(""); // Shows full tree
from abc import ABC, abstractmethod

class FileSystemNode(ABC):
    @abstractmethod
    def get_name(self) -> str: ...
    
    @abstractmethod
    def get_size(self) -> int: ...
    
    @abstractmethod
    def display(self, indent: str = ""): ...

class File(FileSystemNode):
    def __init__(self, name: str, size: int):
        self._name = name
        self._size = size

    def get_name(self) -> str:
        return self._name

    def get_size(self) -> int:
        return self._size

    def display(self, indent: str = ""):
        print(f"{indent}{self._name} ({self._size} bytes)")

class Directory(FileSystemNode):
    def __init__(self, name: str):
        self._name = name
        self._children: list[FileSystemNode] = []

    def add(self, node: FileSystemNode):
        self._children.append(node)

    def remove(self, node: FileSystemNode):
        self._children.remove(node)

    def get_name(self) -> str:
        return self._name

    def get_size(self) -> int:
        return sum(child.get_size() for child in self._children)

    def display(self, indent: str = ""):
        print(f"{indent}{self._name}/")
        for child in self._children:
            child.display(indent + "  ")

    def find(self, pattern: str) -> list[FileSystemNode]:
        results = []
        for child in self._children:
            if pattern in child.get_name():
                results.append(child)
            if isinstance(child, Directory):
                results.extend(child.find(pattern))
        return results

# Usage
root = Directory("project")
src = Directory("src")
src.add(File("main.py", 2048))
src.add(File("utils.py", 1024))
root.add(src)
root.add(File("README.md", 512))

print(root.get_size())  # 3584
root.display()
#include <string>
#include <vector>
#include <memory>
#include <iostream>
#include <numeric>

class FileSystemNode {
public:
    virtual ~FileSystemNode() = default;
    virtual string getName() const = 0;
    virtual long getSize() const = 0;
    virtual void display(const string& indent) const = 0;
};

class File : public FileSystemNode {
    string name_;
    long size_;
public:
    File(string name, long size) : name_(std::move(name)), size_(size) {}
    
    string getName() const override { return name_; }
    long getSize() const override { return size_; }
    
    void display(const string& indent) const override {
        cout << indent << name_ << " (" << size_ << " bytes)" << endl;
    }
};

class Directory : public FileSystemNode {
    string name_;
    vector<shared_ptr<FileSystemNode>> children_;
public:
    explicit Directory(string name) : name_(std::move(name)) {}
    
    void add(shared_ptr<FileSystemNode> node) {
        children_.push_back(std::move(node));
    }
    
    string getName() const override { return name_; }
    
    long getSize() const override {
        long total = 0;
        for (const auto& child : children_) {
            total += child->getSize();
        }
        return total;
    }
    
    void display(const string& indent) const override {
        cout << indent << name_ << "/" << endl;
        for (const auto& child : children_) {
            child->display(indent + "  ");
        }
    }
};

// Usage
auto root = make_shared<Directory>("project");
auto src = make_shared<Directory>("src");
src->add(make_shared<File>("main.cpp", 2048));
src->add(make_shared<File>("utils.cpp", 1024));
root->add(src);
root->add(make_shared<File>("README.md", 512));

cout << root->getSize() << endl; // 3584
root->display("");

When to Use vs When to Avoid

Use Composite When Avoid When
You have a tree/hierarchy structure Data is flat (just use a list)
Individual and group objects should be treated uniformly Leaves and containers have very different interfaces
Recursive operations make sense (size, count, display) No operations span the hierarchy
New node types might be added The structure is simple and fixed

Interview Questions

  1. β€œHow do you handle operations that only make sense on leaves or composites?” – You can either throw UnsupportedOperationException for invalid calls, or split into separate interfaces (safer but slightly more complex).

  2. β€œWhat about cycles?” – A directory containing itself would create infinite recursion. Guard against it with a visited set or by checking parentage before adding.

  3. β€œComposite vs just using a tree data structure?” – Composite gives polymorphism. A generic tree makes you check node types everywhere. Composite lets you call getSize() on any node without knowing if it’s a leaf or branch.

  4. β€œHow do you traverse the tree?” – Depth-first (display shows this naturally), breadth-first (use a queue), or with the Visitor pattern for complex operations.


See It in Action

File System

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access