The composite pattern is a structural design pattern that composes objects into tree structures to represent part-whole hierarchies. A component interface declares operations common to both leaf nodes and composite nodes. Clients call those operations on any node without distinguishing a single object from a group. The composite forwards the call to its children and aggregates the result when needed.
The problem it solves
A file explorer must report the total size of a path. A single file has a fixed size; a folder’s size is the sum of everything inside it, including nested folders. Without a uniform model, client code branches on type: if it is a file, read the size; if it is a folder, loop children and recurse. Every new operation, such as print, search, or permissions, repeats that branching.
The composite pattern treats files and folders as the same kind of node. Both implement a shared interface such as getSize(). A folder stores child nodes and delegates getSize() by summing its children. The client calls getSize() on the root and does not care how deep the tree goes.
Structure
- Component, the interface for leaves and composites; declares operations like
getSize()orrender(). - Leaf, a single object with no children; implements the operation directly.
- Composite, holds a collection of child components; implements the operation by delegating to children and combining results.
- Client, works with any component through the shared interface.
flowchart TB Client["Client"] -->|"calls getSize()"| Component["Component interface"] File["Leaf (File)"] -.->|"implements"| Component Folder["Composite (Folder)"] -.->|"implements"| Component Folder -->|"contains"| File Folder -->|"contains"| Subfolder["Composite (subfolder)"] Subfolder -->|"contains"| File2["Leaf (File)"]
In code
A small file system models files as leaves and directories as composites. Total size at any path uses the same getSize() call whether the node is a file or a folder.
// Component: shared interface for leaves and composites
class File {
constructor(name, sizeInBytes) {
this.name = name;
this.sizeInBytes = sizeInBytes;
}
getName() {
return this.name;
}
getSize() {
return this.sizeInBytes;
}
}
class Directory {
constructor(name) {
this.name = name;
this.children = [];
}
add(child) {
this.children.push(child);
}
getName() {
return this.name;
}
getSize() {
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
}
// Client treats every node the same way
function printSize(node) {
console.log(`${node.getName()}: ${node.getSize()} bytes`);
}
const project = new Directory("project");
const src = new Directory("src");
src.add(new File("app.js", 1200));
src.add(new File("utils.js", 400));
project.add(src);
project.add(new File("README.md", 800));
printSize(project); // project: 2400 bytes
printSize(src); // src: 1600 bytes// Component: shared interface for leaves and composites
interface FileSystemNode {
getName(): string;
getSize(): number;
}
class File implements FileSystemNode {
constructor(
private readonly name: string,
private readonly sizeInBytes: number
) {}
getName(): string {
return this.name;
}
getSize(): number {
return this.sizeInBytes;
}
}
class Directory implements FileSystemNode {
private readonly children: FileSystemNode[] = [];
constructor(private readonly name: string) {}
add(child: FileSystemNode): void {
this.children.push(child);
}
getName(): string {
return this.name;
}
getSize(): number {
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
}
// Client treats every node the same way
function printSize(node: FileSystemNode): void {
console.log(`${node.getName()}: ${node.getSize()} bytes`);
}
const project = new Directory("project");
const src = new Directory("src");
src.add(new File("app.js", 1200));
src.add(new File("utils.js", 400));
project.add(src);
project.add(new File("README.md", 800));
printSize(project); // project: 2400 bytes
printSize(src); // src: 1600 bytes// Component: shared interface for leaves and composites
public interface IFileSystemNode
{
string GetName();
int GetSize();
}
public class File : IFileSystemNode
{
private readonly string _name;
private readonly int _sizeInBytes;
public File(string name, int sizeInBytes)
{
_name = name;
_sizeInBytes = sizeInBytes;
}
public string GetName() => _name;
public int GetSize() => _sizeInBytes;
}
public class Directory : IFileSystemNode
{
private readonly string _name;
private readonly List<IFileSystemNode> _children = new();
public Directory(string name) => _name = name;
public void Add(IFileSystemNode child) => _children.Add(child);
public string GetName() => _name;
public int GetSize() => _children.Sum(child => child.GetSize());
}
// Client treats every node the same way
void PrintSize(IFileSystemNode node) =>
Console.WriteLine($"{node.GetName()}: {node.GetSize()} bytes");
var project = new Directory("project");
var src = new Directory("src");
src.Add(new File("app.js", 1200));
src.Add(new File("utils.js", 400));
project.Add(src);
project.Add(new File("README.md", 800));
PrintSize(project); // project: 2400 bytes
PrintSize(src); // src: 1600 bytesfrom typing import Protocol
# Component: shared interface for leaves and composites
class FileSystemNode(Protocol):
def get_name(self) -> str: ...
def get_size(self) -> int: ...
class File:
def __init__(self, name: str, size_in_bytes: int) -> None:
self._name = name
self._size_in_bytes = size_in_bytes
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return self._size_in_bytes
class Directory:
def __init__(self, name: str) -> None:
self._name = name
self._children: list[FileSystemNode] = []
def add(self, child: FileSystemNode) -> None:
self._children.append(child)
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return sum(child.get_size() for child in self._children)
# Client treats every node the same way
def print_size(node: FileSystemNode) -> None:
print(f"{node.get_name()}: {node.get_size()} bytes")
project = Directory("project")
src = Directory("src")
src.add(File("app.js", 1200))
src.add(File("utils.js", 400))
project.add(src)
project.add(File("README.md", 800))
print_size(project) # project: 2400 bytes
print_size(src) # src: 1600 bytesRelationship to SOLID
Composite supports the SOLID principles. It applies single responsibility by keeping leaf behavior in leaf classes and aggregation in composite classes. It supports open/closed because new node types can be added as new component implementations without changing client code that depends on the interface. It also follows dependency inversion when the client depends on the component abstraction rather than concrete leaf or composite types.
Composite vs alternatives
- Decorator pattern, also shares an interface with the wrapped object, but a decorator wraps one component and adds behavior around a single delegation. A composite holds many children and models a tree, not a linear stack of wrappers.
- Facade pattern, exposes a simpler API over several subsystem classes. A composite keeps the same interface for parts and wholes so clients recurse through the tree uniformly.
- Command pattern, a composite command groups several commands into one undoable unit; that is behavioral composition of operations, while the composite pattern is structural composition of objects in a hierarchy.
- Iterator, walks a structure without defining it. Composite defines the part-whole tree; an iterator (or visitor) is often paired with it to traverse or act on every node.
Trade-offs
- Child management (
add,remove,getChild) does not belong on the leaf interface in the strict form of the pattern. Putting those methods on the component interface forces leaves to throw or no-op. A safer variant exposes child management only on the composite type, or uses a separate optional interface for nodes that accept children. - Deep trees can make naive recursive operations expensive or risk stack overflow; very large hierarchies may need iterative traversal or lazy loading.
- Uniform treatment simplifies clients but can hide type-specific behavior. Not every operation makes sense on every node, so the shared interface should stay narrow.
- Best when the domain is naturally hierarchical (menus, org charts, UI component trees, scene graphs, nested permissions), when clients should ignore whether they hold one object or a subtree, and when operations propagate recursively through the structure.
