Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 28

    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.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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:

    ts
    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:

    Composite tree
    Cart (Composite)
    Root
    Bundle
    Composite child
    LineItem
    Leaf
    LineItem
    Leaf
    Any node may be leaf or branch — same Component interface.
    Uniform client call
    Client
    cart.price()
    No isLeaf check
    Polymorphism
    Composite recurses
    Sum children
    Leaf returns
    Direct value
    Client never branches — recursion lives inside Composite.
    Composite vs flat list
    Fixed depth?
    Flat list OK
    Arbitrary nesting?
    Composite
    Tree operations?
    Walk · sum · render
    DOM / AST / FS
    Classic fit
    Composite earns its cost when depth and nesting vary.

    Informative example

    Before / after — separate paths to Composite:

    ts
    // ❌ Branching everywhere — can't nest bundles
    function total(cart: Cart) {
    let sum = 0
    for (const item of cart.items) sum += item.price * item.qty
    for (const bundle of cart.bundles) {
    for (const item of bundle.items) sum += item.price * item.qty // no nested bundles
    }
    return sum
    }
    // ✅ Composite — arbitrary nesting
    interface 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 depth
    const cart = new Bundle([
    new LineItem(10, 2),
    new Bundle([new LineItem(5, 1), new LineItem(3, 4)]),
    ])

    Execution workflow

    1Composite modeling workflow
    1 / 4

    Define Component

    Interface with operation clients care about.

    price(), render(), hasPermission().

    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.

    4 questions
    1IntermediateQuestionComposite vs plain list of items?+

    Answer

    Composite when nesting arbitrary and operations recurse. Flat list when depth fixed at one.

    Follow-up

    Bundle of bundles example?
    2AdvancedQuestionTransparent vs safe composite?+

    Answer

    Transparent: add/remove on Component — leaves throw or no-op. Safe: child ops only on Composite interface.

    Follow-up

    Which prefer?
    3IntermediateQuestionWhere in DOM?+

    Answer

    Element is Composite; Text node is Leaf. Both implement Node interface. appendChild recurses structure.

    Follow-up

    DocumentFragment?
    4AdvancedQuestionComposite in microservices?+

    Answer

    Aggregator assembles tree of service responses into one document — Composite at API boundary.

    Follow-up

    vs BFF?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.