Composite Pattern #
When building a price calculation feature for an e-commerce system, you might run into this scenario: a “product” can be a single item costing Rp 50,000, or a bundle containing several items, or even a bundle containing other bundles. The code that calculates the total price needs to work the same way for all three — regardless of whether it is processing one item or an entire hierarchy of nested bundles. Without the Composite Pattern, that code fills up with type checks: if isBundle { for each item ... } else { return price }. Every time a new variation is added, the whole conditional block has to be updated. The Composite Pattern eliminates this problem by ensuring that single objects and collections of objects implement the same interface — so client code can call CalculatePrice() on anything without needing to know whether it is a leaf or a composite.
What Is the Composite Pattern? #
The Composite Pattern is a structural design pattern that organizes objects into tree structures to represent part-whole relationships, and lets the client treat individual objects and compositions of objects uniformly through one shared interface.
Two concepts work together:
- Uniformity — leaves and composites implement the same interface; the client does not need to tell them apart
- Recursion — a composite stores a list of children that are also of the same interface type; operations on the composite automatically spread through the entire sub-tree
In Go, the Composite Pattern is very natural because interfaces provide uniformity without inheritance. One interface, two kinds of implementations — a leaf with no children, and a composite that delegates operations to its children.
flowchart TD
subgraph "Composite Tree Structure"
Root["📁 Root\\n(Composite)"]
Root --> A["📁 Documents\\n(Composite)"]
Root --> B["📁 Images\\n(Composite)"]
Root --> C["📄 readme.txt\\n(Leaf)"]
A --> D["📄 invoice.pdf\\n(Leaf)"]
A --> E["📄 contract.docx\\n(Leaf)"]
B --> F["📁 2024\\n(Composite)"]
B --> G["📄 logo.png\\n(Leaf)"]
F --> H["📄 jan.jpg\\n(Leaf)"]
F --> I["📄 feb.jpg\\n(Leaf)"]
end
Client -->|"GetSize()\\nsame call for all"| Root
Root -->|"GetSize()\\nrecursive"| A & B & C
A -->|"GetSize()"| D & EThe Problem It Solves #
Without the Composite Pattern, code that processes hierarchical structures usually ends up with two big, interrelated problems.
Problem 1: Type Checks Scattered Everywhere #
// ANTI-PATTERN: the client must know the concrete type of every node
type File struct {
Name string
Size int64
}
type Folder struct {
Name string
Files []*File // can only hold Files — no nested folders
Subfolders []*Folder
}
// The client must handle File and Folder differently
func calculateTotalSize(folder *Folder) int64 {
var total int64
for _, file := range folder.Files {
total += file.Size // handle File
}
for _, sub := range folder.Subfolders {
total += calculateTotalSize(sub) // handle Folder differently
}
return total
}
// Problem: if a new type appears (e.g., Symlink), every function must change
Problem 2: Duplicated Logic for Every Type #
Functions like calculateTotalSize, printTree, search, compress — all of them need separate handling for File and Folder. Every time a new type is added, all those functions must be updated. This massively violates the Open-Closed Principle.
// CORRECT: one interface, the client does not need to know the type
type FileSystemNode interface {
GetName() string
GetSize() int64
Print(indent int)
}
// File (Leaf) and Folder (Composite) are both FileSystemNode
func calculateTotalSize(node FileSystemNode) int64 {
return node.GetSize() // the same for all types — recursive polymorphism
}
Three Main Components #
The Composite Pattern consists of three roles to understand before implementing.
classDiagram
class FileSystemNode {
<<interface>>
+GetName() string
+GetSize() int64
+GetPath() string
+Print(indent int)
+Search(query string) []FileSystemNode
}
class File {
-name string
-size int64
-path string
-mimeType string
+GetName() string
+GetSize() int64
+GetPath() string
+Print(indent int)
+Search(query string) []FileSystemNode
}
class Folder {
-name string
-path string
-children []FileSystemNode
+GetName() string
+GetSize() int64
+GetPath() string
+Print(indent int)
+Search(query string) []FileSystemNode
+Add(node FileSystemNode)
+Remove(name string) bool
+GetChildren() []FileSystemNode
}
FileSystemNode <|.. File : Leaf
FileSystemNode <|.. Folder : Composite
Folder o-- FileSystemNode : children| Component | Role | Implementation |
|---|---|---|
| Component | Interface implemented by every node | FileSystemNode |
| Leaf | Node with no children — the smallest unit | File, MenuItem, Employee |
| Composite | Node with children — delegates to them | Folder, MenuGroup, Department |
Full Implementation: File System #
The file system is the most classic and most intuitive example of the Composite Pattern — everyone is already familiar with files and folders that can nest.
Component Interface #
package filesystem
import "time"
// FileSystemNode is the Component interface — the contract for every node in the tree.
// Both File and Folder implement this interface.
type FileSystemNode interface {
GetName() string
GetSize() int64 // for File: the file size; for Folder: the total size of everything inside
GetPath() string
GetModifiedAt() time.Time
Print(indent int) // show the structure with indentation
Search(query string) []FileSystemNode // find nodes by name
Count() int // node count: File=1, Folder=total of everything inside it, recursively
}
Leaf: File #
package filesystem
import (
"fmt"
"strings"
"time"
)
// File is the Leaf — no children, the smallest unit in the tree.
type File struct {
name string
size int64
path string
mimeType string
modifiedAt time.Time
}
func NewFile(name, path, mimeType string, size int64) *File {
return &File{
name: name,
size: size,
path: path,
mimeType: mimeType,
modifiedAt: time.Now(),
}
}
func (f *File) GetName() string { return f.name }
func (f *File) GetSize() int64 { return f.size }
func (f *File) GetPath() string { return f.path }
func (f *File) GetModifiedAt() time.Time { return f.modifiedAt }
func (f *File) GetMIMEType() string { return f.mimeType }
// Print shows the file with the appropriate indentation.
func (f *File) Print(indent int) {
fmt.Printf("%s📄 %s (%s, %s)\n",
strings.Repeat(" ", indent),
f.name,
f.mimeType,
humanizeSize(f.size),
)
}
// Search returns itself if the name matches, or an empty slice if not.
func (f *File) Search(query string) []FileSystemNode {
if strings.Contains(strings.ToLower(f.name), strings.ToLower(query)) {
return []FileSystemNode{f}
}
return nil
}
// Count always returns 1 — a File is a single node.
func (f *File) Count() int { return 1 }
// humanizeSize converts bytes into a human-readable format.
func humanizeSize(bytes int64) string {
switch {
case bytes >= 1<<30:
return fmt.Sprintf("%.1f GB", float64(bytes)/(1<<30))
case bytes >= 1<<20:
return fmt.Sprintf("%.1f MB", float64(bytes)/(1<<20))
case bytes >= 1<<10:
return fmt.Sprintf("%.1f KB", float64(bytes)/(1<<10))
default:
return fmt.Sprintf("%d B", bytes)
}
}
Composite: Folder #
package filesystem
import (
"fmt"
"strings"
"time"
)
// Folder is the Composite — it holds children that are also FileSystemNode.
// All operations are delegated recursively to the children.
type Folder struct {
name string
path string
children []FileSystemNode
modifiedAt time.Time
}
func NewFolder(name, path string) *Folder {
return &Folder{
name: name,
path: path,
children: make([]FileSystemNode, 0),
modifiedAt: time.Now(),
}
}
func (f *Folder) GetName() string { return f.name }
func (f *Folder) GetPath() string { return f.path }
func (f *Folder) GetModifiedAt() time.Time { return f.modifiedAt }
// GetSize returns the total size of everything in the folder, recursively.
// The client does not need to know this involves recursion — just call GetSize().
func (f *Folder) GetSize() int64 {
var total int64
for _, child := range f.children {
total += child.GetSize() // automatic recursion — every child knows how to size itself
}
return total
}
// Print shows the folder and everything inside it, recursively.
func (f *Folder) Print(indent int) {
fmt.Printf("%s📁 %s/ (%s, %d items)\n",
strings.Repeat(" ", indent),
f.name,
humanizeSize(f.GetSize()),
len(f.children),
)
for _, child := range f.children {
child.Print(indent + 1) // every child knows how to display itself
}
}
// Search looks for nodes by name across the whole sub-tree, recursively.
func (f *Folder) Search(query string) []FileSystemNode {
var results []FileSystemNode
// Check whether the folder itself matches
if strings.Contains(strings.ToLower(f.name), strings.ToLower(query)) {
results = append(results, f)
}
// Delegate the search to every child
for _, child := range f.children {
results = append(results, child.Search(query)...)
}
return results
}
// Count returns the total number of nodes in the sub-tree (including itself).
func (f *Folder) Count() int {
total := 1 // the folder itself
for _, child := range f.children {
total += child.Count()
}
return total
}
// Add adds a node to this folder.
func (f *Folder) Add(node FileSystemNode) {
f.children = append(f.children, node)
f.modifiedAt = time.Now()
}
// Remove deletes a node by name. Returns true if it was successfully removed.
func (f *Folder) Remove(name string) bool {
for i, child := range f.children {
if child.GetName() == name {
f.children = append(f.children[:i], f.children[i+1:]...)
f.modifiedAt = time.Now()
return true
}
}
return false
}
// GetChildren returns a copy of the children list.
func (f *Folder) GetChildren() []FileSystemNode {
result := make([]FileSystemNode, len(f.children))
copy(result, f.children)
return result
}
// FindFolder looks for a subfolder by name (one level only).
func (f *Folder) FindFolder(name string) *Folder {
for _, child := range f.children {
if folder, ok := child.(*Folder); ok && folder.GetName() == name {
return folder
}
}
return nil
}
Usage: The Client Does Not Care About Types #
func main() {
// Build the file system structure
root := filesystem.NewFolder("root", "/")
documents := filesystem.NewFolder("documents", "/documents")
documents.Add(filesystem.NewFile("invoice_2024.pdf", "/documents/invoice_2024.pdf", "application/pdf", 245760))
documents.Add(filesystem.NewFile("contract.docx", "/documents/contract.docx", "application/docx", 102400))
reports := filesystem.NewFolder("reports", "/documents/reports")
reports.Add(filesystem.NewFile("q1_2024.xlsx", "/documents/reports/q1_2024.xlsx", "application/xlsx", 512000))
reports.Add(filesystem.NewFile("q2_2024.xlsx", "/documents/reports/q2_2024.xlsx", "application/xlsx", 487424))
documents.Add(reports)
images := filesystem.NewFolder("images", "/images")
images.Add(filesystem.NewFile("logo.png", "/images/logo.png", "image/png", 51200))
images.Add(filesystem.NewFile("banner.jpg", "/images/banner.jpg", "image/jpeg", 204800))
root.Add(documents)
root.Add(images)
root.Add(filesystem.NewFile("readme.txt", "/readme.txt", "text/plain", 1024))
// The client calls the same operations on every node — it does not care Leaf or Composite
fmt.Println("=== File System Structure ===")
root.Print(0)
fmt.Printf("\nTotal size: %s\n", humanizeSize(root.GetSize()))
fmt.Printf("Total nodes: %d\n", root.Count())
fmt.Println("\n=== Search for 'q' ===")
results := root.Search("q")
for _, node := range results {
fmt.Printf("Found: %s (%s)\n", node.GetName(), node.GetPath())
}
// The same operations work on sub-folders too
fmt.Printf("\nDocuments size: %s\n", humanizeSize(documents.GetSize()))
fmt.Printf("Reports size: %s\n", humanizeSize(reports.GetSize()))
}
The resulting output:
=== File System Structure ===
📁 root/ (1.5 MB, 3 items)
📁 documents/ (1.3 MB, 3 items)
📄 invoice_2024.pdf (application/pdf, 240.0 KB)
📄 contract.docx (application/docx, 100.0 KB)
📁 reports/ (976.0 KB, 2 items)
📄 q1_2024.xlsx (application/xlsx, 500.0 KB)
📄 q2_2024.xlsx (application/xlsx, 476.0 KB)
📁 images/ (249.0 KB, 2 items)
📄 logo.png (image/png, 50.0 KB)
📄 banner.jpg (image/jpeg, 200.0 KB)
📄 readme.txt (text/plain, 1.0 KB)
Total size: 1.5 MB
Total nodes: 8
Second Case Study: Organization Chart #
An organization chart is a highly relevant Composite Pattern use case in enterprise applications — every employee can be a manager with reports, and every manager can report to another manager.
package org
import (
"fmt"
"strings"
)
// Employee is the Component interface for every node in the org chart.
type Employee interface {
GetName() string
GetTitle() string
GetSalary() float64 // total salary: for an individual=their salary, for a manager=salary+reports
GetHeadcount() int // total people below (including themselves)
Print(indent int)
GetDepartment() string
}
// IndividualContributor is the Leaf — no direct reports.
type IndividualContributor struct {
name string
title string
salary float64
department string
}
func NewIC(name, title, department string, salary float64) *IndividualContributor {
return &IndividualContributor{name: name, title: title, salary: salary, department: department}
}
func (e *IndividualContributor) GetName() string { return e.name }
func (e *IndividualContributor) GetTitle() string { return e.title }
func (e *IndividualContributor) GetSalary() float64 { return e.salary }
func (e *IndividualContributor) GetHeadcount() int { return 1 }
func (e *IndividualContributor) GetDepartment() string { return e.department }
func (e *IndividualContributor) Print(indent int) {
fmt.Printf("%s👤 %s (%s) — Rp %.0f\n",
strings.Repeat(" ", indent), e.name, e.title, e.salary)
}
// Manager is the Composite — it has direct reports that are also Employees.
type Manager struct {
name string
title string
baseSalary float64
department string
directReports []Employee
}
func NewManager(name, title, department string, baseSalary float64) *Manager {
return &Manager{
name: name,
title: title,
baseSalary: baseSalary,
department: department,
directReports: make([]Employee, 0),
}
}
func (m *Manager) AddReport(e Employee) {
m.directReports = append(m.directReports, e)
}
func (m *Manager) GetName() string { return m.name }
func (m *Manager) GetTitle() string { return m.title }
func (m *Manager) GetDepartment() string { return m.department }
// GetSalary returns the total salary of the whole team — recursively downward.
func (m *Manager) GetSalary() float64 {
total := m.baseSalary
for _, report := range m.directReports {
total += report.GetSalary()
}
return total
}
// GetHeadcount returns the total number of people under this manager (including themselves).
func (m *Manager) GetHeadcount() int {
total := 1 // themselves
for _, report := range m.directReports {
total += report.GetHeadcount()
}
return total
}
func (m *Manager) Print(indent int) {
fmt.Printf("%s👔 %s (%s) — Rp %.0f [team total: Rp %.0f, %d people]\n",
strings.Repeat(" ", indent),
m.name, m.title,
m.baseSalary,
m.GetSalary(),
m.GetHeadcount(),
)
for _, report := range m.directReports {
report.Print(indent + 1)
}
}
Usage:
func main() {
// Build the org chart
cto := org.NewManager("Budi", "CTO", "Engineering", 50_000_000)
backendLead := org.NewManager("Citra", "Backend Lead", "Engineering", 30_000_000)
backendLead.AddReport(org.NewIC("Dani", "Senior Engineer", "Engineering", 20_000_000))
backendLead.AddReport(org.NewIC("Eka", "Mid Engineer", "Engineering", 15_000_000))
backendLead.AddReport(org.NewIC("Fajar", "Junior Engineer", "Engineering", 10_000_000))
frontendLead := org.NewManager("Gita", "Frontend Lead", "Engineering", 28_000_000)
frontendLead.AddReport(org.NewIC("Hadi", "Senior Engineer", "Engineering", 20_000_000))
frontendLead.AddReport(org.NewIC("Indah", "Mid Engineer", "Engineering", 15_000_000))
cto.AddReport(backendLead)
cto.AddReport(frontendLead)
cto.AddReport(org.NewIC("Joko", "DevOps Engineer", "Engineering", 22_000_000))
// Call the same operations at every level — the client does not care IC or Manager
fmt.Println("=== Organization Chart ===")
cto.Print(0)
fmt.Printf("\nTotal engineering headcount: %d people\n", cto.GetHeadcount())
fmt.Printf("Total engineering salary budget: Rp %.0f\n", cto.GetSalary())
fmt.Printf("Backend team salary budget: Rp %.0f\n", backendLead.GetSalary())
}
Composite with More Complex Operations #
The Composite Pattern also supports operations that need state during traversal, not just simple aggregation.
// The Apply operation — runs a function on every node in the tree.
// Useful for bulk operations: compress all files, update permissions, etc.
func Apply(node FileSystemNode, fn func(FileSystemNode)) {
fn(node)
if folder, ok := node.(*Folder); ok {
for _, child := range folder.GetChildren() {
Apply(child, fn) // recurse through the whole sub-tree
}
}
}
// Collect — gathers every node that satisfies a predicate.
func Collect(node FileSystemNode, predicate func(FileSystemNode) bool) []FileSystemNode {
var results []FileSystemNode
Apply(node, func(n FileSystemNode) {
if predicate(n) {
results = append(results, n)
}
})
return results
}
// Usage: find all PDFs larger than 1MB
largePDFs := Collect(root, func(n FileSystemNode) bool {
file, ok := n.(*File)
return ok && file.GetMIMEType() == "application/pdf" && file.GetSize() > 1<<20
})
// Usage: log every node during traversal
Apply(root, func(n FileSystemNode) {
log.Printf("Processing: %s", n.GetPath())
})
sequenceDiagram
participant C as Client
participant R as Root Folder
participant D as Documents Folder
participant F1 as invoice.pdf
participant R2 as Reports Folder
participant F2 as q1.xlsx
C->>R: GetSize()
R->>D: GetSize()
D->>F1: GetSize() → 245760
D->>R2: GetSize()
R2->>F2: GetSize() → 512000
R2-->>D: 512000
D-->>R: 245760 + 102400 + 512000 + 487424
R-->>C: total of everything inside
Note over C,F2: The client calls once — recursion happens behind the scenesPreventing Cyclic References #
One of the biggest risks of the Composite Pattern is a cyclic reference — folder A contains folder B, which contains folder A again. This causes infinite recursion and stack overflow.
// ANTI-PATTERN: no protection against cyclic references
func (f *Folder) Add(node FileSystemNode) {
f.children = append(f.children, node)
// if node is an ancestor of f, a cycle forms — GetSize() will loop forever
}
// CORRECT: validate before adding a node
func (f *Folder) Add(node FileSystemNode) error {
// Check whether the node is an ancestor of this folder
if f.isAncestorOf(node) {
return fmt.Errorf("cannot add %q to %q: would create cyclic reference", node.GetName(), f.name)
}
f.children = append(f.children, node)
f.modifiedAt = time.Now()
return nil
}
// isAncestorOf checks whether the target is an ancestor of this folder.
func (f *Folder) isAncestorOf(target FileSystemNode) bool {
if f == target {
return true
}
for _, child := range f.children {
if childFolder, ok := child.(*Folder); ok {
if childFolder.isAncestorOf(target) {
return true
}
}
}
return false
}
Cyclic References Cause Stack Overflow
If a composite tree has a cyclic reference, every recursive operation —
GetSize(),Print(),Search()— will run forever until the goroutine exhausts its stack and panics. Always validate that no cycle exists before adding a node to the tree, especially if nodes can be added dynamically from user input.
Testing the Composite Pattern #
Testing the Composite Pattern focuses on two aspects: operations on a single Leaf work correctly, and operations on a Composite correctly delegate to all its children.
func TestFile_GetSize(t *testing.T) {
file := filesystem.NewFile("test.pdf", "/test.pdf", "application/pdf", 102400)
if file.GetSize() != 102400 {
t.Errorf("expected size 102400, got %d", file.GetSize())
}
if file.Count() != 1 {
t.Errorf("expected count 1 for leaf, got %d", file.Count())
}
}
func TestFolder_GetSize_AggregatesChildren(t *testing.T) {
folder := filesystem.NewFolder("test", "/test")
folder.Add(filesystem.NewFile("a.txt", "/test/a.txt", "text/plain", 1000))
folder.Add(filesystem.NewFile("b.txt", "/test/b.txt", "text/plain", 2000))
subFolder := filesystem.NewFolder("sub", "/test/sub")
subFolder.Add(filesystem.NewFile("c.txt", "/test/sub/c.txt", "text/plain", 3000))
folder.Add(subFolder)
expectedSize := int64(1000 + 2000 + 3000)
if folder.GetSize() != expectedSize {
t.Errorf("expected total size %d, got %d", expectedSize, folder.GetSize())
}
if folder.Count() != 4 { // the folder itself + 3 files
t.Errorf("expected count 4, got %d", folder.Count())
}
}
func TestFolder_Search_FindsInSubTree(t *testing.T) {
root := filesystem.NewFolder("root", "/")
sub := filesystem.NewFolder("documents", "/documents")
sub.Add(filesystem.NewFile("invoice.pdf", "/documents/invoice.pdf", "application/pdf", 1000))
root.Add(sub)
root.Add(filesystem.NewFile("readme.txt", "/readme.txt", "text/plain", 500))
// Search "invoice" from the root — it must reach into the subfolder
results := root.Search("invoice")
if len(results) != 1 {
t.Errorf("expected 1 result, got %d", len(results))
}
if results[0].GetName() != "invoice.pdf" {
t.Errorf("expected 'invoice.pdf', got %q", results[0].GetName())
}
}
func TestFolder_Remove(t *testing.T) {
folder := filesystem.NewFolder("test", "/test")
folder.Add(filesystem.NewFile("a.txt", "/test/a.txt", "text/plain", 1000))
folder.Add(filesystem.NewFile("b.txt", "/test/b.txt", "text/plain", 2000))
removed := folder.Remove("a.txt")
if !removed {
t.Error("expected Remove to return true")
}
if folder.Count() != 2 { // folder + b.txt
t.Errorf("expected count 2 after remove, got %d", folder.Count())
}
if folder.Remove("nonexistent.txt") {
t.Error("expected Remove to return false for nonexistent file")
}
}
func TestUniformTreatment(t *testing.T) {
// Test that the same operations work for Leaf and Composite
nodes := []filesystem.FileSystemNode{
filesystem.NewFile("file.txt", "/file.txt", "text/plain", 1000),
func() filesystem.FileSystemNode {
f := filesystem.NewFolder("folder", "/folder")
f.Add(filesystem.NewFile("child.txt", "/folder/child.txt", "text/plain", 500))
return f
}(),
}
for _, node := range nodes {
// These operations must work for every type without type assertions
_ = node.GetName()
_ = node.GetSize()
_ = node.GetPath()
_ = node.Count()
_ = node.Search("test")
// No if/else based on type — this is the uniformity of the Composite Pattern
}
}
When to Use and When Not to #
USE Composite if:
✓ The data has a natural hierarchical (tree) structure
✓ The client needs to treat leaves and composites the same way
✓ Operations are recursive — they apply to one node and its entire sub-tree
✓ You want to add new node types without changing client code
✓ You want to eliminate type-based if/else in code that processes trees
AVOID Composite if:
✗ The structure is not hierarchical — a graph, flat list, or many-to-many relationships
✗ Leaves and Composites have very different operations — the interface becomes too wide
✗ The tree is very deep and recursive operations could cause stack overflow
✗ You need access to the parent node from a child — a standard Composite keeps no parent reference
Composite in the Go Standard Library
io.MultiWriteris a Composite Pattern example in the standard library — it combines severalio.Writers and delegates everyWrite()to all registered writers.http.Handleris also frequently combined in a composite way through middleware chains. Understanding the Composite Pattern helps you read and understand these patterns faster.
Composite Review Checklist #
DESIGN:
□ The Component interface defines operations relevant to ALL nodes (leaf and composite)
□ Composite methods (Add, Remove, GetChildren) live on the Composite, not the interface
□ Interface operations are recursive — meaningful for both leaves and composites
□ No type assertions in client code
IMPLEMENTATION:
□ The Leaf implements every interface method with correct values (not panics)
□ The Composite delegates to all its children, not implementing operations itself
□ There is protection against cyclic references in the Add() method
□ GetChildren() returns a copy, not a reference to the internal slice
RECURSION:
□ Every recursive operation has a clear base case (Leaf = termination)
□ No possibility of infinite recursion (no cycles in the tree)
□ Heavy operations (GetSize on a large tree) are considered for caching
TESTING:
□ Test operations on a single Leaf
□ Test operations on a Composite with children
□ Test recursive operations on a multi-level tree
□ Test cyclic reference detection (if implemented)
□ Test that the interface is called uniformly without type assertions
Summary #
- Composite enables uniformity — Leaf and Composite implement the same interface, so the client can call the same operations without knowing the type.
- Three main components: the Component interface, the Leaf (smallest unit with no children), and the Composite (delegates operations to children recursively).
- Recursive operations are the main strength —
GetSize(),Search(),Print(),Count()all run automatically through the entire sub-tree; the client only calls once at the root.- Composite methods live only on the Composite, not the interface —
Add(),Remove(),GetChildren()are absent from the Component interface because the Leaf does not need (and cannot) implement them.- Protect against cyclic references — validate in
Add()before accepting a node; a cyclic reference causes an unrecoverable stack overflow.ApplyandCollectare very useful helper functions for bulk operations over the whole tree without changing the Component interface.- Examples in the standard library:
io.MultiWriterand HTTP middleware chains use the Composite principle — delegating operations to a set of implementations behind one interface.- Don’t use it for non-hierarchical structures — if the data is a graph or flat list, the Composite Pattern is over-engineering that makes the code harder to understand.