← Back to principles

Design Principle

Composite Pattern

Learn the Composite pattern: compose objects into tree structures to represent part-whole hierarchies. Treat individual objects and compositions uniformly.

Composite Pattern

Why This Matters

Think of the Composite pattern like a file system. You have files (individual objects) and folders (composite objects). Folders can contain files or other folders. But from the outside, you treat them the same—you can get the size of a file or a folder (which sums up its contents). The Composite pattern does the same for code—it lets you treat individual objects and groups of objects uniformly.

This matters because many systems have hierarchical structures: file systems, UI components (containers and widgets), organization charts, expression trees. The Composite pattern provides a clean way to represent these hierarchies and operate on them uniformly. Without it, you'd need separate code for individual objects and groups.

In interviews, when someone asks "How would you represent a tree structure?", they're testing whether you understand the Composite pattern. Do you know how to treat individual and composite objects uniformly? Can you build recursive structures? Most engineers can't. They use separate classes and wonder why the code is complex.

What Engineers Usually Get Wrong

Engineers often think "Composite pattern is just tree structures." But Composite pattern is more specific—it's about treating individual and composite objects uniformly through a common interface. This allows recursive operations (like calculating total size of a folder by summing its contents) without the client knowing whether it's dealing with a file or a folder.

Engineers also don't understand when to use Composite. Use Composite when you have part-whole hierarchies and want to treat individual and composite objects uniformly. Don't use it for simple collections—use regular collections instead. Composite adds complexity, so use it only when you need the uniform interface.

How This Breaks Systems in the Real World

A service was building a UI component system. They had separate classes for individual components and containers. Code that operated on components had to check "is this a container?" and handle containers differently. The code was complex and error-prone. The fix? Use Composite pattern. Create a common interface for components and containers. Now code can operate on both uniformly, without special cases.

Another story: A service was using Composite pattern but didn't handle cycles. A folder could contain itself (directly or indirectly), creating infinite loops. Operations like "calculate total size" would hang. The fix? Detect cycles. Use visited sets or depth limits to prevent infinite recursion.


What is the Composite Pattern?

Composite Pattern provides:

  • Tree structure: Represent hierarchical structures
  • Uniform treatment: Treat individual and composite objects the same
  • Recursive composition: Composites can contain other composites
  • Simplified client code: Client doesn't need to distinguish between leaf and composite

Use cases:

  • File system (files and folders)
  • UI components (containers and widgets)
  • Organization structures
  • Expression trees

Structure

Component (interface)
  ├─ operation()
  ├─ add(component)
  ├─ remove(component)
  └─ getChild(index)

Leaf (implements Component)
  └─ operation()

Composite (implements Component)
  └─ children: Component[]
  └─ operation() (calls operation on all children)
  └─ add(component)
  └─ remove(component)

Examples

File System Example

1// Component interface
2interface FileSystemComponent {
3 getName(): string;
4 getSize(): number;
5 display(indent: string): void;
6}
7
8// Leaf (File)
9class File implements FileSystemComponent {
10 constructor(private name: string, private size: number) {}
11
12 getName(): string {
13 return this.name;
14 }

UI Component Example

1// Component interface
2interface UIComponent {
3 render(): void;
4 add(component: UIComponent): void;
5 remove(component: UIComponent): void;
6 getChild(index: number): UIComponent | null;
7}
8
9// Leaf (Button)
10class Button implements UIComponent {
11 constructor(private label: string) {}
12
13 render(): void {

Common Pitfalls

  • Leaf operations: Adding operations to leaf that don't make sense. Fix: Use null object pattern or throw exceptions
  • Performance: Traversing large trees can be slow. Fix: Use caching, lazy evaluation
  • Circular references: Composites can reference themselves. Fix: Add cycle detection
  • Too many operations: Component interface becomes too large. Fix: Use visitor pattern for complex operations

Interview Questions

Beginner

Q: What is the Composite pattern and when would you use it?

A:

Composite Pattern composes objects into tree structures to represent part-whole hierarchies.

Key characteristics:

  • Tree structure: Represents hierarchical structures
  • Uniform treatment: Treat individual and composite objects the same
  • Recursive composition: Composites can contain other composites
  • Simplified client: Client doesn't distinguish between leaf and composite

Example:

1// Both File and Directory implement same interface
2interface FileSystemComponent {
3 getSize(): number;
4 display(): void;
5}
6
7// Leaf
8class File implements FileSystemComponent { /* ... */ }
9
10// Composite
11class Directory implements FileSystemComponent {
12 private children: FileSystemComponent[] = [];
13 // Can contain Files or other Directories
14}

Use cases:

  • File systems: Files and folders
  • UI components: Containers and widgets
  • Organization structures: Departments and employees
  • Expression trees: Operations and operands

Intermediate

Q: Explain how the Composite pattern works. How do you handle operations that don't apply to leaves?

A:

Composite Pattern Structure:

1. Component Interface:

1interface Component {
2 operation(): void;
3 add(component: Component): void;
4 remove(component: Component): void;
5}

2. Leaf:

1class Leaf implements Component {
2 operation(): void {
3 // Leaf operation
4 }
5
6 add(component: Component): void {
7 throw new Error("Cannot add to leaf");
8 }
9
10 remove(component: Component): void {
11 throw new Error("Cannot remove from leaf");
12 }
13}

3. Composite:

1class Composite implements Component {
2 private children: Component[] = [];
3
4 operation(): void {
5 // Operate on all children
6 this.children.forEach(child => child.operation());
7 }
8
9 add(component: Component): void {
10 this.children.push(component);
11 }
12
13 remove(component: Component): void

Handling Leaf Operations:

Option 1: Throw Exception

1class Leaf implements Component {
2 add(component: Component): void {
3 throw new Error("Cannot add to leaf");
4 }
5}

Option 2: Do Nothing

1class Leaf implements Component {
2 add(component: Component): void {
3 // Do nothing
4 }
5}

Option 3: Null Object Pattern

1class NullComponent implements Component {
2 add(component: Component): void {
3 // Do nothing
4 }
5}

Senior

Q: Design a composite system for a document editor that supports nested sections, paragraphs, images, and tables. Handle operations like word count, export, and formatting that work differently for different component types.

A:

1// Component interface
2interface DocumentComponent {
3 getWordCount(): number;
4 export(format: ExportFormat): string;
5 format(style: Style): void;
6 add(component: DocumentComponent): void;
7 remove(component: DocumentComponent): void;
8}
9
10// Leaf - Paragraph
11class Paragraph implements DocumentComponent {
12 private text: string = "";
13 private style: Style |

Features:

  1. Uniform interface: All components implement same interface
  2. Type-specific behavior: Each type handles operations differently
  3. Recursive operations: Operations propagate through tree
  4. Flexible composition: Mix different component types

  • Composite pattern: Represents part-whole hierarchies

  • Uniform treatment: Treat individual and composite objects the same

  • Tree structure: Builds tree structures recursively

  • Use cases: File systems, UI components, document structures

  • Leaf operations: Handle operations that don't apply to leaves

  • Best practices: Use exceptions or null object for leaf operations, consider performance

  • Trees - Composite pattern is based on tree structures. Understanding trees helps understand composite pattern.

  • Recursion & Backtracking - Composite operations are naturally recursive. Understanding recursion helps implement composite operations.

  • SOLID Principles - Composite pattern follows Open/Closed Principle (extend without modify). Understanding SOLID helps understand composite benefits.

  • Separation of Concerns - Composite pattern treats individual and composite uniformly. Understanding separation of concerns helps understand composite benefits.

  • Decorator Pattern - Both patterns compose objects. Composite composes tree structures, Decorator composes features.

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.