Composite Pattern
Composite lets a single object and a group share the same interface, so callers recurse a tree without caring whether a node is leaf or branch.
Introduction
Composite lets a single object and a group share the same interface, so callers recurse a tree without caring whether a node is leaf or branch. Filesystem trees, GUI widgets, org charts, permission groups, pricing bundles, and AST nodes all use the same part-whole pattern.
The story
A pricing engine handled item discounts on one code path and bundle discounts on another. "Bundle of bundles" became impossible. Modeling line items and bundles as PriceComponent with common price() made arbitrary nesting trivial.
The business problem
Separate code paths for "one" vs "many" don't scale to nested structures:
- Branching everywhere: if (isGroup) scattered at every call site.
- Depth limits: hard-coded nesting levels — "bundle of bundles" blocked.
- Duplicated recursion: each consumer reimplements tree walk.
- Inconsistent totals: leaf and group priced by different rules that drift.
The problem teams faced
Composite fits when:
- Structure is tree-shaped — parts contain parts.
- Clients should treat individual and group uniformly.
- Operations recurse (price, render, permission check, size).
- Nesting depth varies and may grow over time.
Understanding the topic
Intent: Compose objects into tree structures; treat individual and compositions uniformly.
- Component — common interface (price, render, getSize).
- Leaf — no children; implements operation directly.
- Composite — holds children; operation recurses over them.
- Client — calls component.operation() — never branches on type.
Internal architecture
Pricing composite:
interface PriceComponent {price(): Money}class LineItem implements PriceComponent {constructor(private unitPrice: Money, private qty: number) {}price() { return this.unitPrice.times(this.qty) }}class Bundle implements PriceComponent {private children: PriceComponent[] = []add(c: PriceComponent) { this.children.push(c) }price() { return this.children.reduce((sum, c) => sum.add(c.price()), Money.zero()) }}
Visual explanation
Three diagrams cover tree structure, uniform client, and pricing example:
Informative example
Before / after — separate paths to Composite:
// ❌ Branching everywhere — can't nest bundlesfunction total(cart: Cart) {let sum = 0for (const item of cart.items) sum += item.price * item.qtyfor (const bundle of cart.bundles) {for (const item of bundle.items) sum += item.price * item.qty // no nested bundles}return sum}// ✅ Composite — arbitrary nestinginterface PriceComponent { price(): number }class LineItem implements PriceComponent {constructor(private price: number, private qty: number) {}price() { return this.price * this.qty }}class Bundle implements PriceComponent {constructor(private children: PriceComponent[]) {}price() { return this.children.reduce((s, c) => s + c.price(), 0) }}// Client: cart.price() works for any depthconst cart = new Bundle([new LineItem(10, 2),new Bundle([new LineItem(5, 1), new LineItem(3, 4)]),])
Execution workflow
Define Component
Interface with operation clients care about.
Real-world use
DOM (Element vs fragment), compiler ASTs, filesystems, GUI widget hierarchies, org charts, permission User/Group, CloudFormation nested stacks, GraphQL field aggregation trees.
Production case study
SaaS permission system:
- Scenario: Users and groups modeled with separate code paths.
- Decision: Principal interface with User (leaf) and Group (composite).
- Outcome: Nested groups arbitrary depth; one hasPermission() walk.
- Metric: permission check code paths 3 → 1.
Trade-offs
- Pro: uniform client code; arbitrary nesting; recursion encapsulated.
- Pro: new node types via new Leaf/Composite implementations.
- Con: overkill for flat lists with fixed structure.
- Con: child management on Component interface may not fit all leaves (safe ops).
Decision framework
- Use when structure is tree and depth varies.
- Use when operations naturally recurse (sum, render, traverse).
- Avoid for flat collections with no nesting.
- Consider transparent vs safe composite (leaf add() throws vs no-op).
Best practices
- Keep child management off Leaf if using safe composite — only Composite gets add/remove.
- Visitor pattern pairs well for many operations on same tree.
- Immutable composites simplify reasoning — build tree, then freeze.
Anti-patterns to avoid
- Composite for flat list "because patterns".
- Client code with instanceof Leaf checks — defeats uniform treatment.
- Mutable shared children arrays without defensive copy.
Common mistakes
- Cycles in composite tree — infinite recursion without cycle detection.
- Deep trees blowing stack — iterative traversal for very deep structures.
- Putting leaf-only fields on Component interface — use role interfaces.
Debugging tips
- Log tree depth and node count when totals look wrong.
- Visualize tree structure in tests for complex bundles.
Optimization strategies
- Memoize composite.price() when subtree immutable and queried repeatedly.
- Lazy-build composite children for large trees.
Common misconceptions
- Composite ≠ simple list. Need part-whole hierarchy with uniform ops.
- Aggregator microservice is Composite at network scale.
- Composite + Visitor when many operations traverse same tree.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionComposite vs plain list of items?+
Answer
Follow-up
2AdvancedQuestionTransparent vs safe composite?+
Answer
Follow-up
3IntermediateQuestionWhere in DOM?+
Answer
Follow-up
4AdvancedQuestionComposite in microservices?+
Answer
Follow-up
Summary
You can model part-whole hierarchies with Composite and eliminate isGroup branching. Tell the bundle-of-bundles pricing story — if you can do that, you own Composite.