LESSON 5.3
Composite
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
16 min read
Real-world analogy
A corporate organizational hierarchy consists of individual employees and management departments. An individual software engineer is a leaf with a salary. A product division is a composite containing engineering managers, who in turn manage teams of engineers. When the CEO requests the total payroll of the organization, they do not iterate through different payroll procedures for individuals versus departments; they simply invoke calculatePayroll() at the root department, which recursively sums payroll across all children.
The problem
You are building an in-memory file system. The file system contains two primary entities: binary File objects (which store a byte size directly) and Directory objects (which hold other files and subdirectories).
Without the Composite pattern, client code that calculates disk usage or prints a tree structure must continuously check the type of each item using instanceof or conditional type checks:
// Naive implementation: Conditional type checking and disparate interfaces
class File {
constructor(name, size) {
this.name = name;
this.size = size;
}
}
class Directory {
constructor(name) {
this.name = name;
this.files = [];
this.subdirectories = [];
}
}
function calculateTotalSize(item) {
// Client must know concrete types and traverse disparate structures
if (item instanceof File) {
return item.size;
} else if (item instanceof Directory) {
let sum = 0;
for (const f of item.files) sum += f.size;
for (const d of item.subdirectories) sum += calculateTotalSize(d);
return sum;
}
return 0;
}public class Example {
static class File {
String name;
long size;
public File(String name, long size) { this.name = name; this.size = size; }
}
static class Directory {
String name;
This approach introduces brittle conditional branching across your codebase. Adding symbolic links, archives, or shortcuts forces updates to every traversal and calculation function.
The solution
The Composite pattern organizes objects into tree structures to represent part-whole hierarchies. It defines a common interface for both individual primitives (Leaf) and complex containers (Composite), allowing clients to treat them uniformly.
- Component (
FileSystemEntry): Declares common operations (getSize(),display()) for all objects in the hierarchy. - Leaf (
File): Represents primitive terminal elements that have no children. - Composite (
Directory): Stores child components and delegates operations recursively across its children. - Client: Interacts with all elements exclusively through the
Componentcontract.
Step-by-step implementation
Step 1: Declare the common component contract
Create an abstract component declaring the operations that leaves and composites share.
Step 2: Implement the leaf element
The leaf handles operations directly without holding references to child nodes.
Step 3: Implement the composite container
The composite maintains an array of child FileSystemEntry components and aggregates results recursively.
The complete example
Here is the complete, runnable implementation showing nested directories and files executed uniformly.
+ Directory: root
+ Directory: etc
- File: hosts (320 bytes)
- File: passwd (1250 bytes)
+ Directory: home
+ Directory: swarup
- File: resume.pdf (45000 bytes)
- File: notes.txt (1500 bytes)
- File: boot.bin (8192 bytes)
Total tree storage: 56262 bytesVariations
Uniform vs safe child management
A foundational design decision in the Composite pattern is where child management methods (add, remove, getChildren) are declared:
- Uniformity (Transparency): Child methods are declared directly on the base
Componentinterface. BothFileandDirectoryshare the exact same interface. The client never needs to know if a node is a leaf or a composite. However, callingadd()on a leaf must throw anUnsupportedOperationExceptionor silently fail, sacrificing compile-time safety. - Safety: Child methods are declared only on the concrete
Compositeclass (Directory). Leaves do not haveadd()orremove()methods, ensuring invalid operations are rejected at compile time. However, clients lose uniformity because they must cast components toCompositeto manipulate children.
Parent pointers and aggregate caching
In deep composite trees, repeatedly traversing the tree to calculate metrics (such as directory size or total price) degrades performance.
To optimize this:
- Each component maintains a reference to its
parent. - Each composite caches its aggregate calculation.
- When a leaf changes (size updated, child added or removed), it invalidates the cache upwards through its parent pointer until reaching the root.
Where you've already used it
- Browser DOM:
Nodeis the component;TextandCommentare leaves;ElementandDocumentFragmentare composites containing child nodes. - React Component Trees: React elements and native JSX primitives form a composite tree passed via
props.children. - Java Swing / AWT:
java.awt.Componentis the base;JButtonandJLabelare leaves;java.awt.Container(andJPanel) are composites. - JSON Abstract Syntax Trees: A JSON document consists of scalar primitives (
string,number,booleanleaves) and container nodes (object,arraycomposites).
Pros and cons
Advantages
- Uniform Client Code: Clients work with complex tree structures through a simple, unified interface without conditional branching.
- Extensibility: Adding new Leaf or Composite types adheres to the Open/Closed Principle without modifying existing tree traversal algorithms.
- Hierarchical Clarity: Accurately models real-world part-whole relationships (file systems, scene graphs, bills of materials).
Disadvantages
- Overly General Interface: Can make it difficult to restrict which components can be children of a specific composite at compile time.
- Deep Recursion Overhead: Traversing deep composite trees can lead to stack overflow or memory pressure without caching or iterative traversal.
When not to use it
Do not use Composite if your data is linear or tabular rather than hierarchical. If your entities never contain nested versions of themselves, standard list or map structures are simpler and more performant.
Related patterns
| Pattern | Purpose | Structure |
|---|---|---|
| Composite | Treats individual objects and part-whole trees uniformly | Tree hierarchy |
| Decorator | Adds dynamic behavior to an object transparently | Single-child recursive chain |
| Flyweight | Shares shared leaf nodes across trees to conserve memory | Shared immutable instances |
| Visitor | Adds operations to composite trees without changing node classes | Externalized traversal operations |
| Iterator | Traverses composite collections sequentially | Cursor abstraction |
Composite: virtual file system
Build a virtual file system where Files (leaves) and Directories (composites) share a uniform component interface without manual type checking.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
What is the core difference between the Uniformity and Safety approaches in Composite? Uniformity defines child management on the root Component for maximum transparency at the cost of runtime exceptions on leaves. Safety defines child management only on Composites, ensuring compile-time safety but requiring clients to distinguish leaves from composites.
How do you prevent circular references in a Composite tree? A composite must validate that the node being added is not an ancestor of itself. This can be verified by traversing upward through parent pointers before permitting the insertion.
How does Composite relate to the Decorator pattern? Both share a common interface with the objects they wrap. However, Composite manages multiple children representing a part-whole tree, whereas Decorator wraps a single child to layer on additional behavior.
How can you visit nodes in a Composite structure without polluting the Component interface with new methods?
Combine Composite with the Visitor pattern. The Component interface defines a single accept(Visitor v) method, allowing new operations (serialization, validation, syntax checks) to be added without modifying the tree classes.
Try it
In-memory File System LLD
Model a directory tree with path resolution, safe moves and consistent mutations.