# Composite

> Treat single objects and nested groups of them through one interface, so code can walk a tree of any depth without checking node types.

- kind: pattern
- category: structural
- aliases: Object Tree, Part-Whole Hierarchy
- canonical: https://aipatterns.dev/design-patterns/composite
- inspired-by: https://refactoring.guru/design-patterns/composite

## Intent

Represent part-whole hierarchies as a tree in which leaves and containers share one interface. A container implements each operation by delegating to its children and combining their results, so client code calls the same method on a single item or on a whole subtree.

## Forces

- **Uniform client code** vs **two kinds of node**: callers want `node.monthlyCost()`, not `if (node is Group) ... else ...` at every use.
- **Arbitrary depth** vs **fixed loops**: nested loops work for exactly two levels; org charts, folders and cost centers have as many as they like.
- **Type safety** vs **one interface**: putting `add(child)` on leaves keeps the interface uniform but lets callers do nonsense; leaving it off means callers need to know which node they hold.
- **Recomputing on every call** vs **caching aggregates**: a deep tree recalculates its totals on every read unless you cache, and caching needs invalidation.

## Therefore

**Define one component interface for the operations that make sense on both a single item and a group; leaves implement them directly, and composites implement them by looping over their children.**

Recursion does the work: a composite does not know whether its children are leaves or other composites. Keep child management (`add`, `remove`) on the composite type unless callers truly need to build trees through the component interface.

## Structure

```mermaid
classDiagram
  class CostNode {
    <<interface>>
    +name() String
    +monthlyCost() Money
    +lines(depth) List
  }
  class Resource {
    -Money cost
    +monthlyCost() Money
    +lines(depth) List
  }
  class Group {
    -CostNode[] children
    +add(node) Group
    +monthlyCost() Money
    +lines(depth) List
  }
  class BudgetAlert {
    +check(node, limit) List
  }
  CostNode <|.. Resource
  CostNode <|.. Group
  Group o-- CostNode : children
  BudgetAlert ..> CostNode
```

### Participants

- **Component** (`CostNode`): the operations shared by leaves and containers.
- **Leaf** (`Resource`): a node with no children that does the real work (here, knows its own cost).
- **Composite** (`Group`): holds child components, delegates to them, and combines the results.
- **Client** (`BudgetAlert`): works through the component interface and never checks which kind it holds.

## Use it when

- Your data is a tree of unbounded depth: folders, org units, menu items, cost centers, UI widgets, bill-of-materials parts.
- Clients repeat the same `instanceof`/type switch to tell single items from groups.
- An operation on a group is the combination of the same operation on its members: totals, counts, rendering, validation, enable/disable.
- You want to pass a single item wherever a group is accepted, and the reverse.

## Do not use it when

- The tree has a fixed, known shape, such as "teams contain resources" and nothing deeper. Two nested loops over plain data are clearer.
- Leaves and groups share almost no operations. Forcing them into one interface fills leaves with methods that throw `UnsupportedOperationException`. Keep two types.
- The node types are stable but the operations change often. A data-only tree (a TypeScript discriminated union, a Java `sealed` interface with records, Python dataclasses with `match`) plus one recursive function per operation is simpler; see also [Visitor](https://aipatterns.dev/design-patterns/visitor.md).
- The structure is really a graph with shared children or cycles. Composite assumes each node has one parent; use a graph model and an explicit traversal with a visited set.
- You only need to flatten nested lists once. A recursive generator or `flatMap` does it without new types.

## Application grill

**Inspect first:** the tree shapes and the code that walks them; whether nesting is user-controlled (menus, org charts, comment threads) or fixed; existing recursion or iteration helpers.

**Missing pieces:** none beyond the language: one interface for leaf and group. Deep, user-controlled trees need an explicit stack instead of recursion, so hostile depth cannot exhaust the call stack.

**Ask before you build:**

- **Scale:** how deep can a tree get, and is depth bounded by construction or by user input?
- **Performance:** does a small change re-walk the whole tree, and is incremental update needed?
- **Reliability:** can a cycle exist (a child pointing at its ancestor), and where is that rejected?
- **Budget:** one node interface simplifies callers, but every node kind now implements it - is the type count owned?
- **Implementation:** are node kinds a closed set (a switch is fine) or open (visitors and plugins must extend)?

## Implementation steps

1. Confirm the domain is a recursive tree and list the operations that apply to both single items and groups.
2. Declare the component interface with only those operations.
3. Implement leaf classes for the concrete items.
4. Implement the composite: a child collection, `add`/`remove`, and each operation as a loop over children that combines results.
5. Replace client type checks with calls on the component interface.
6. If aggregates are expensive and the tree is large, add caching in the composite with explicit invalidation on `add`/`remove`, and measure first.

## Example

A FinOps tool rolls up monthly cloud spend across nested groups (company, department, team) and flags any node over budget, whether it is one database or a whole department.

```ts title="costs.ts"
const usd = (cents: number) => `$${(cents / 100).toFixed(2)}`;

interface CostNode {
  readonly name: string;
  monthlyCost(): number; // cents
  lines(depth?: number): string[];
}

class Resource implements CostNode {
  constructor(readonly name: string, private readonly cents: number) {}
  monthlyCost(): number {
    return this.cents;
  }
  lines(depth = 0): string[] {
    return [`${"  ".repeat(depth)}${this.name}  ${usd(this.cents)}`];
  }
}

class Group implements CostNode {
  private readonly children: CostNode[] = [];
  constructor(readonly name: string) {}

  add(...nodes: CostNode[]): this {
    this.children.push(...nodes);
    return this;
  }
  monthlyCost(): number {
    return this.children.reduce((sum, c) => sum + c.monthlyCost(), 0);
  }
  lines(depth = 0): string[] {
    const head = `${"  ".repeat(depth)}${this.name}/  ${usd(this.monthlyCost())}`;
    return [head, ...this.children.flatMap((c) => c.lines(depth + 1))];
  }
}

// Client: one rule, any node.
function overBudget(node: CostNode, limitCents: number): string | null {
  const cost = node.monthlyCost();
  return cost > limitCents ? `${node.name} is ${usd(cost - limitCents)} over budget` : null;
}

const payments = new Group("payments").add(
  new Resource("postgres-primary", 84000),
  new Resource("api-cluster", 126000),
);
const search = new Group("search").add(new Resource("opensearch", 61000));
const company = new Group("acme").add(
  new Group("engineering").add(payments, search),
  new Resource("office-vpn", 9000),
);

console.log(company.lines().join("\n"));
for (const node of [company, payments, new Resource("ci-runners", 45000)]) {
  console.log(overBudget(node, 200000) ?? `${node.name} ok`);
}
```

```python title="costs.py"
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Protocol

def usd(cents: int) -> str:
    return f"${cents / 100:.2f}"

class CostNode(Protocol):
    name: str

    def monthly_cost(self) -> int: ...
    def lines(self, depth: int = 0) -> list[str]: ...

@dataclass
class Resource:
    name: str
    cents: int

    def monthly_cost(self) -> int:
        return self.cents

    def lines(self, depth: int = 0) -> list[str]:
        return [f"{'  ' * depth}{self.name}  {usd(self.cents)}"]

@dataclass
class Group:
    name: str
    children: list[CostNode] = field(default_factory=list)

    def add(self, *nodes: CostNode) -> Group:
        self.children.extend(nodes)
        return self

    def monthly_cost(self) -> int:
        return sum(c.monthly_cost() for c in self.children)

    def lines(self, depth: int = 0) -> list[str]:
        head = f"{'  ' * depth}{self.name}/  {usd(self.monthly_cost())}"
        return [head] + [line for c in self.children for line in c.lines(depth + 1)]

def over_budget(node: CostNode, limit_cents: int) -> str | None:
    cost = node.monthly_cost()
    return f"{node.name} is {usd(cost - limit_cents)} over budget" if cost > limit_cents else None

if __name__ == "__main__":
    payments = Group("payments").add(Resource("postgres-primary", 84000), Resource("api-cluster", 126000))
    search = Group("search").add(Resource("opensearch", 61000))
    company = Group("acme").add(Group("engineering").add(payments, search), Resource("office-vpn", 9000))

    print("\n".join(company.lines()))
    for node in (company, payments, Resource("ci-runners", 45000)):
        print(over_budget(node, 200000) or f"{node.name} ok")
```

```go title="costs.go"
package main

import (
	"fmt"
	"strings"
)

func usd(cents int) string { return fmt.Sprintf("$%.2f", float64(cents)/100) }

type CostNode interface {
	Name() string
	MonthlyCost() int // cents
	Lines(depth int) []string
}

type Resource struct {
	name  string
	cents int
}

func (r Resource) Name() string     { return r.name }
func (r Resource) MonthlyCost() int { return r.cents }
func (r Resource) Lines(depth int) []string {
	return []string{fmt.Sprintf("%s%s  %s", strings.Repeat("  ", depth), r.name, usd(r.cents))}
}

type Group struct {
	name     string
	children []CostNode
}

func NewGroup(name string, children ...CostNode) *Group {
	return &Group{name: name, children: children}
}

func (g *Group) Add(nodes ...CostNode) { g.children = append(g.children, nodes...) }
func (g *Group) Name() string         { return g.name }

func (g *Group) MonthlyCost() int {
	sum := 0
	for _, c := range g.children {
		sum += c.MonthlyCost()
	}
	return sum
}

func (g *Group) Lines(depth int) []string {
	out := []string{fmt.Sprintf("%s%s/  %s", strings.Repeat("  ", depth), g.name, usd(g.MonthlyCost()))}
	for _, c := range g.children {
		out = append(out, c.Lines(depth+1)...)
	}
	return out
}

func overBudget(n CostNode, limit int) string {
	if cost := n.MonthlyCost(); cost > limit {
		return fmt.Sprintf("%s is %s over budget", n.Name(), usd(cost-limit))
	}
	return n.Name() + " ok"
}

func main() {
	payments := NewGroup("payments", Resource{"postgres-primary", 84000}, Resource{"api-cluster", 126000})
	search := NewGroup("search", Resource{"opensearch", 61000})
	company := NewGroup("acme", NewGroup("engineering", payments, search), Resource{"office-vpn", 9000})

	fmt.Println(strings.Join(company.Lines(0), "\n"))
	for _, n := range []CostNode{company, payments, Resource{"ci-runners", 45000}} {
		fmt.Println(overBudget(n, 200000))
	}
}
```

```java title="Costs.java"
import java.util.ArrayList;
import java.util.List;

public class Costs {
    static String usd(long cents) { return String.format("$%.2f", cents / 100.0); }

    interface CostNode {
        String name();
        long monthlyCost(); // cents
        List<String> lines(int depth);
    }

    record Resource(String name, long cents) implements CostNode {
        public long monthlyCost() { return cents; }
        public List<String> lines(int depth) {
            return List.of("  ".repeat(depth) + name + "  " + usd(cents));
        }
    }

    static final class Group implements CostNode {
        private final String name;
        private final List<CostNode> children = new ArrayList<>();

        Group(String name, CostNode... nodes) {
            this.name = name;
            children.addAll(List.of(nodes));
        }

        Group add(CostNode node) { children.add(node); return this; }
        public String name() { return name; }

        public long monthlyCost() {
            return children.stream().mapToLong(CostNode::monthlyCost).sum();
        }

        public List<String> lines(int depth) {
            List<String> out = new ArrayList<>();
            out.add("  ".repeat(depth) + name + "/  " + usd(monthlyCost()));
            children.forEach(c -> out.addAll(c.lines(depth + 1)));
            return out;
        }
    }

    static String overBudget(CostNode node, long limit) {
        long cost = node.monthlyCost();
        return cost > limit ? node.name() + " is " + usd(cost - limit) + " over budget" : node.name() + " ok";
    }

    public static void main(String[] args) {
        Group payments = new Group("payments",
            new Resource("postgres-primary", 84000), new Resource("api-cluster", 126000));
        Group search = new Group("search", new Resource("opensearch", 61000));
        Group company = new Group("acme",
            new Group("engineering", payments, search), new Resource("office-vpn", 9000));

        System.out.println(String.join("\n", company.lines(0)));
        for (CostNode n : List.of(company, payments, new Resource("ci-runners", 45000))) {
            System.out.println(overBudget(n, 200000));
        }
    }
}
```

`overBudget` receives a company, a team and a single resource and handles all three the same way. That uniformity is the whole point.

## Trade-offs

**Gains**
- Client code is type-check-free and works at any depth.
- New leaf or composite types plug in without client changes.
- Aggregation logic lives in one place, the composite, instead of in every caller.
- Trees can be built from configuration or data at runtime.

**Costs**
- The component interface tends to be either too wide (leaves carry meaningless child methods) or too narrow (clients cast to reach `add`).
- It is harder to enforce constraints such as "a team may not contain a company" through the type system.
- Naive aggregates are recomputed on every call; `lines` above recomputes subtotals at each level.
- Deep recursion can overflow the stack on pathological inputs; untrusted trees may need an explicit stack.

## Relations

- [Decorator](https://aipatterns.dev/design-patterns/decorator.md) has a similar recursive shape, but wraps exactly one component to add behavior. A composite holds many children and combines their results.
- [Iterator](https://aipatterns.dev/design-patterns/iterator.md) walks a composite tree without exposing its internal structure.
- [Visitor](https://aipatterns.dev/design-patterns/visitor.md) adds new operations over a composite tree without editing every node class.
- [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) often runs along a composite's parent links, passing a request from a leaf up to its ancestors.
- [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) can share identical leaves to save memory in very large trees, as long as they hold no parent reference.

## Agent checklist

- [ ] The structure is a real tree of variable depth, not a fixed two-level grouping.
- [ ] The component interface contains only operations meaningful for both leaves and composites.
- [ ] No client code checks whether a node is a leaf or a composite after the change.
- [ ] Each node has at most one parent, and cycles are impossible or rejected on `add`.
- [ ] Tests cover an empty composite, a single leaf, and a nested tree at least three levels deep.
