Skip to content

Composite pattern explained

When you build software, you often deal with hierarchies: a folder contains files, a UI panel contains buttons, or a manager supervises employees. Some elements can stand alone (a file, a button, an employee), while others contain many more (a folder, a panel, a department). So the composite pattern comes in when we want to interact with both individual objects and groups of objects in the same way, it gives us a uniform interface for treating single items and collections the same. Instead of scattering special cases (is it a file? is it a folder?) across your code, you deal with them through one consistent abstraction. This leads to:

  • Cleaner code: No need for if/else everywhere to check whether you’re handling a file or a folder.
  • Scalability: Easily extend your system with new object types (e.g., new UI components, new file types).

Using composite pattern to work with files and folders.

When working with folders even in our computers we need to run operations like; showing name of item, calculating total size or deleting an item. Without the Composite Pattern, we would need to handle files and folders separately, with different logic everywhere which could get messy. To better understand this the composite pattern has three main things;

  • A Leaf: the simplest element in the structure (cannot contain children).
  • A Composite: a container that can hold other Components (including both leaves and other composites).
  • A Component interface: the common contract they both implement.

Now in our files and folders example; we introduce a common Component interface that both Leaf objects (files) and Composite objects (folders) follow. This means client code doesn’t need to care whether it’s handling a single file or an entire folder — it just calls the same methods (getSize(), display(), etc.), and the right behavior happens. Check the code below.

js
// Component interface (abstract base class)
// Defines common operations for both File (leaf) and Folder (composite)
class FileSystemItem {
  constructor(name) {
    this.name = name; // Every file/folder has a name
  }

  // Method to get size (must be implemented by subclasses)
  getSize() { 
    throw new Error("Must override"); 
  }

  // Method to display structure (must be implemented by subclasses)
  display(indent = 0) { 
    throw new Error("Must override"); 
  }
}

// Leaf class: File
// Represents an indivisible element (no children)
class File extends FileSystemItem {
  constructor(name, size) {
    super(name);
    this.size = size; // Size of the file in KB
  }

  // Returns file size directly
  getSize() {
    return this.size;
  }

  // Displays the file name with indentation (tree visualization)
  display(indent = 0) {
    console.log(`${' '.repeat(indent)}📄 ${this.name} (${this.size}KB)`);
  }
}

// Composite class: Folder
// Can contain Files (leaves) or other Folders (composites)
class Folder extends FileSystemItem {
  constructor(name) {
    super(name);
    this.children = []; // Holds nested files/folders
  }

  // Add a file or folder to this folder
  add(item) {
    this.children.push(item);
  }

  // Calculate total size by summing up sizes of all children
  getSize() {
    return this.children.reduce((acc, child) => acc + child.getSize(), 0);
  }

  // Display folder name and recursively display all its children
  display(indent = 0) {
    console.log(`${' '.repeat(indent)}📁 ${this.name}`);
    this.children.forEach(child => child.display(indent + 2));
  }
}

The above code defines a common base class FileSystemItem with two abstract methods: getSize() and display(). The leaf class File extends this base to represent individual files, each with a name and size, returning its own size and displaying itself with indentation for tree-like visualization. The composite class Folder also extends the base but can contain multiple children (either File or Folder objects). It calculates its size by summing the sizes of all children and recursively displays its contents in a structured tree format. This allows both files and folders to be treated uniformly, letting client code work with single items or whole hierarchies in the same way.

Usage

To make use of the above code;

js
const root = new Folder("root");

const file1 = new File("resume.pdf", 120);
const file2 = new File("photo.png", 850);

const docs = new Folder("Documents");
docs.add(file1);

const images = new Folder("Images");
images.add(file2);

root.add(docs);
root.add(images);

root.display();
console.log("Total size:", root.getSize(), "KB");

But!

When hierarchy is simple it beats logic to use this composite pattern also it is not advisable if performance is critical as recursion can be expensive in deep trees.

Enjoy every byte.