AlgoMaster Logo

Composite Design Pattern

Ashish

Ashish Pratap Singh

5 min read

The Composite Design Pattern is a structural pattern that lets you treat individual objects and compositions of objects uniformly.

It allows you to build tree-like structures (e.g., file systems, UI hierarchies, organizational charts) where clients can work with both single elements and groups of elements using the same interface.

It’s particularly useful in situations where:

  • You need to represent part-whole hierarchies.
  • You want to perform operations on both leaf nodes and composite nodes in a consistent way.
  • You want to avoid writing special-case logic to distinguish between "single" and "grouped" objects.

When designing such systems, developers often start with if-else blocks or type checks to handle individual items differently from collections. For example, a render() method might have to check whether the element is a button, a panel, or a container before deciding what to do.

But as the structure grows in complexity, this approach becomes hard to scale, violates the Open/Closed Principle, and introduces tight coupling between the client code and the structure's internal composition.

The Composite Pattern solves this by defining a common interface for all elements, whether they are leaves or composites. Each component can then be treated the same way — allowing the client to operate on complex structures as if they were simple objects.

Let’s walk through a real-world example to see how we can apply the Composite Pattern to model a flexible, hierarchical system that’s both clean and extensible.

1. The Problem: Modeling a File Explorer

Imagine you're building a file explorer application (like Finder on macOS or File Explorer on Windows). The system needs to represent:

  • Files – simple items that have a name and a size.
  • Folders – containers that can hold files and other folders (even nested folders).

Your goal is to support operations such as:

  • getSize() – return the total size of a file or folder (which is the sum of all contents).
  • printStructure() – print the name of the item, including indentation to show hierarchy.
  • delete() – delete a file or a folder and everything inside it.

The Naive Approach

A straightforward solution might involve two separate classes: File and Folder. Here's a simplified version:

File

Folder

What’s Wrong With This Approach?

As the structure grows more complex, this solution introduces several critical problems:

1. Repetitive Type Checks

Operations like getSize()printStructure(), and delete() require repeated instanceof checks and downcasting — leading to duplicated and fragile logic.

2. No Shared Abstraction

There’s no common interface for File and Folder, which means you can’t treat them uniformly. You can't write code like:

3. Violation of Open/Closed Principle

To add new item types (e.g., ShortcutCompressedFolder), you must modify existing logic in every place where type checks happen — increasing the risk of bugs and regression.

4. Lack of Recursive Elegance

Deleting deeply nested folders or computing sizes across multiple levels becomes a tangled mess of nested conditionals and recursive checks.

What We Really Need

We need a solution that:

  • Introduces a common interface (e.g., FileSystemItem) for all components.
  • Allows files and folders to be treated uniformly via polymorphism.
  • Enables folders to contain a list of the same interface, supporting arbitrary nesting.
  • Supports recursive operations like delete and getSize without type checks.
  • Makes the system easy to extend — new item types can be added without modifying existing logic.

This is exactly the kind of problem the Composite Design Pattern is made for.

2. What is the Composite Pattern

The Composite Design Pattern is a structural design pattern that lets you treat individual objects and groups of objects in a uniform way.

In a composite structure, each node in the hierarchy shares the same interface, whether it’s a leaf (e.g., a File) or a composite (e.g., a Folder). This allows clients to perform operations like getSize()delete(), or render() recursively and consistently across both.

Class Diagram

Component Interface (e.g., FileSystemItem)

Declares the common interface for all concrete components

Leaf (e.g., File)

Represents an end object (no children)

Composite (e.g., Folder)

Represents an object that can hold children (including other composites)

Client (e.g., FileExplorerApp)

Works with components using the shared interface

3. Implementing Composite

We’ll start by defining a common interface for all items in our file system, allowing both files and folders to be treated uniformly.

1. Define the Component Interface

This interface ensures that all file system items — whether they are files or folders — expose the same behavior to the client.

2. Create the Leaf Class – File

Each File is a leaf node. It doesn’t contain any children.

3. Create the Composite Class – Folder

The Folder class is a composite. It can contain both File and Folder instances, making it recursive and scalable.

4. Client Code

Output

With the Composite Pattern, we’ve modeled the file system the way it naturally works, as a tree of items where some are leaves and others are containers. Each operation (getSize()printStructure()delete()) is now modular, recursive, and extensible.

What We Achieved with Composite?

  • Unified treatment: Files and folders share a common interface, allowing polymorphism
  • Clean recursion: No instanceof, no casting — just delegation
  • Scalability: Easily supports deeply nested structures
  • Maintainability: Adding new file types (e.g., Shortcut, CompressedFolder) is easy
  • Extensibility: New operations can be added via interface extension or visitor pattern