low-level-design3 min read

Composite Design Pattern Tutorial from Scratch (2026)

Composite Design Pattern Tutorial from Scratch (2026)

Published:  |  Category: Low Level Design  |  Reading time: ~15 min
Composite Design Pattern Tutorial from Scratch (2026)

The Composite design pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

This pattern is essential when you need to work with tree-like data where both leaf and composite nodes should be treated identically. Common examples include file systems, GUI component trees, and organizational hierarchies.

Understanding Tree Structures and the Composite Pattern

The Composite pattern defines three key participants: the Component interface declaring common operations, Leaf objects that perform actual work, and Composite objects that store child components and delegate operations to them.

Both Leaf and Composite implement the same Component interface, enabling uniform treatment. When an operation is invoked on a Composite, it recursively calls the operation on all its children.

// Component interface
class FileSystemNode {
  constructor(name) {
    this.name = name;
  }
  display(indent = 0) {
    throw new Error('Override required');
  }
  getSize() {
    throw new Error('Override required');
  }
}

// Leaf
class File extends FileSystemNode {
  constructor(name, size) {
    super(name);
    this.size = size;
  }
  display(indent = 0) {
    console.log(' '.repeat(indent) + '- ' + this.name);
  }
  getSize() {
    return this.size;
  }
}

// Composite
class Directory extends FileSystemNode {
  constructor(name) {
    super(name);
    this.children = [];
  }
  add(node) {
    this.children.push(node);
  }
  remove(node) {
    this.children = this.children.filter(c => c !== node);
  }
  display(indent = 0) {
    console.log(' '.repeat(indent) + '+ ' + this.name);
    for (const child of this.children) {
      child.display(indent + 2);
    }
  }
  getSize() {
    return this.children.reduce((sum, c) => sum + c.getSize(), 0);
  }
}

// Usage
const root = new Directory('root');
const docs = new Directory('docs');
docs.add(new File('readme.md', 100));
docs.add(new File('license.txt', 50));
root.add(docs);
root.add(new File('index.js', 200));
root.display();
console.log('Total size:', root.getSize());

Real-World File System Example with Uniform Operations

Beyond display and size calculation, uniform operations like search, permission checks, and serialization benefit from the Composite pattern. A client never needs to differentiate between a File and a Directory when performing these operations.

Adding new operation methods to the Component interface instantly extends behavior across the entire tree structure without modifying client code.

class SearchResult {
  constructor(name, path) {
    this.name = name;
    this.path = path;
  }
}

// Extend FileSystemNode with search
FileSystemNode.prototype.search = function(name, path = '') {
  throw new Error('Override required');
};

File.prototype.search = function(name, path) {
  const currentPath = path + '/' + this.name;
  if (this.name.includes(name)) {
    return [new SearchResult(this.name, currentPath)];
  }
  return [];
};

Directory.prototype.search = function(name, path = '') {
  const currentPath = path + '/' + this.name;
  let results = [];
  if (this.name.includes(name)) {
    results.push(new SearchResult(this.name, currentPath));
  }
  for (const child of this.children) {
    results = results.concat(child.search(name, currentPath));
  }
  return results;
};

const results = root.search('doc');
results.forEach(r => console.log(r.path));

Frequently Asked Questions

When should I use the Composite design pattern?

Use Composite when you have tree structures where individual objects and compositions should be treated uniformly. It works well for file systems, UI component hierarchies, nested menus, and organizational charts where operations apply to both leaf and composite nodes.

What is the difference between Leaf and Composite in the Composite pattern?

A Leaf has no children and implements the actual behavior of an operation. A Composite holds child Components (which may be Leaves or other Composites) and delegates operations to them, often aggregating results. Both share the same Component interface.

Does the Composite pattern support type safety?

There is a trade-off between type safety and transparency. A design where Leaf and Composite expose different methods (e.g., add/remove only on Composite) is type-safe but requires downcasting. A unified Component interface with add/remove is transparent but less safe.

What are common drawbacks of the Composite pattern?

The Composite pattern can make the design overly general. Since all components share the same interface, you may need runtime checks to verify types. Additionally, managing child constraints (e.g., max children, allowed types) can be difficult with a fully uniform interface.

Originally published on Ayodhyyya. Last updated June 1, 2026.