Bridge connected two hierarchies; today the structure grows inward. A restaurant's menu in PideYa is not a flat list: it is a tree. Sections ("Pizzas", "Drinks") containing subsections ("Drinks > Soft Drinks", "Drinks > Wines") containing dishes; and combos that group dishes —and sometimes other combos— under a joint price. Every time the code asks "is this a dish or a section?" before operating, it writes an if that will be repeated in twenty places. Composite removes that question: leaves and groups share an interface, and the whole tree is treated the same as a single object.

Contents

  1. The problem in PideYa: the menu is a tree
  2. Intent and structure of the pattern
  3. Complete Java implementation
  4. Recursive traversals
  5. Combos: when the group is also for sale
  6. Transparency versus safety
  7. When to use it and when not to
  8. Common mistakes, exercises, and conclusion

The problem in PideYa: the menu is a tree

The menu of La Bella Napoli, one of PideYa's star restaurants, looks like this:

La Bella Napoli Menu
├── Pizzas
│   ├── Classics
│   │   ├── Margarita ........ €8.50
│   │   └── Prosciutto ....... €9.90
│   └── Specials
│       └── Tartufo .......... €13.50
├── Pastas
│   └── Carbonara ............ €10.50
└── Drinks
    ├── Water ................ €1.50
    └── Soft Drinks
        └── Cola ............. €2.20

Arbitrary depth: sections within sections, with no agreed limit. The team's first model used two unrelated classes and separate collections:

// First attempt: two separate worlds. Do NOT imitate.
public class Section {
    private String name;
    private List<Section> subsections;
    private List<Dish> dishes;
}

And every piece of code touching the menu ended up infested with the same double loop plus question:

// To display the menu... plus another copy for availability... plus another for search...
void display(Section section, int level) {
    print(section.getName(), level);
    for (Dish d : section.getDishes()) {           // branch 1: leaves
        print(d.getName() + " " + d.getPrice(), level + 1);
    }
    for (Section s : section.getSubsections()) {   // branch 2: groups
        display(s, level + 1);
    }
}

Every new operation (is this section available? how many gluten-free dishes are there? what should show if the kitchen shuts down the grill?) duplicates the two-branch structure. And when the business asks for a combo containing other combos, the two parallel lists will have nothing left to give. The symptom, stated cleanly: a recursive part-whole structure handled with conditionals instead of a common interface.

Intent and structure of the pattern

Intent (GoF): compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

The pattern's two ideas, worth keeping apart:

  1. A common interface (Component) for leaves and groups: the operations that make sense in both (display, isAvailable...).
  2. The group contains Components, not concrete leaves or groups: that is why unlimited nesting comes for free — a section containing sections containing dishes is simply a Composite with Component children.
classDiagram
    class MenuComponent {
        <<interface>>
        +getName() String
        +isAvailable() boolean
        +countAvailableDishes() int
        +display(level: int)
    }
    class Dish {
        -name: String
        -price: BigDecimal
        -available: boolean
        +isAvailable() boolean
        +countAvailableDishes() int
        +display(level: int)
    }
    class MenuSection {
        -name: String
        -children: List~MenuComponent~
        +add(child: MenuComponent)
        +remove(child: MenuComponent)
        +isAvailable() boolean
        +countAvailableDishes() int
        +display(level: int)
    }
    class ClientApp

    MenuComponent <|.. Dish
    MenuComponent <|.. MenuSection
    MenuSection *-- MenuComponent : children
    ClientApp --> MenuComponent : uses
GoF role In PideYa
Component (common interface) MenuComponent
Leaf (leaf, no children) Dish
Composite (group, with Component children) MenuSection (and later Combo)
Client The app, the search feature, the availability engine

The key arrow in the diagram is the composition MenuSection *-- MenuComponent — the same filled-diamond relationship we learned with Order *-- OrderLine in the UML lesson, and here it is also recursive: the composite contains elements of its own interface. That arrow looping back is the pattern's visual signature.

Complete Java implementation

The Component — only operations that make sense on leaves and on groups:

public interface MenuComponent {
    String getName();
    boolean isAvailable();
    int countAvailableDishes();
    void display(int level);
}

The leaf: a dish answers for itself, no recursion:

public class Dish implements MenuComponent {

    private final String name;
    private final BigDecimal price;
    private boolean available = true;   // the kitchen can run out of it

    public Dish(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    @Override public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
    public void markSoldOut() { this.available = false; }

    @Override
    public boolean isAvailable() { return available; }

    @Override
    public int countAvailableDishes() { return available ? 1 : 0; }

    @Override
    public void display(int level) {
        String indent = "  ".repeat(level);
        System.out.println(indent + name + " ... €" + price
                + (available ? "" : " [SOLD OUT]"));
    }
}

The composite: a section delegates to its children without knowing whether they are dishes or subsections:

public class MenuSection implements MenuComponent {

    private final String name;
    private final List<MenuComponent> children = new ArrayList<>();

    public MenuSection(String name) { this.name = name; }

    public MenuSection add(MenuComponent child) {
        children.add(child);
        return this;              // fluent, in the style of module 2's Builder
    }

    public void remove(MenuComponent child) { children.remove(child); }

    @Override public String getName() { return name; }

    /** A section is available if ANY child is. */
    @Override
    public boolean isAvailable() {
        return children.stream().anyMatch(MenuComponent::isAvailable);
    }

    /** Adds up whatever the children say; the recursion propagates on its own. */
    @Override
    public int countAvailableDishes() {
        return children.stream().mapToInt(MenuComponent::countAvailableDishes).sum();
    }

    @Override
    public void display(int level) {
        System.out.println("  ".repeat(level) + "== " + name + " ==");
        for (MenuComponent child : children) {
            child.display(level + 1);
        }
    }
}

Building the tree and using it — the client never asks what it is holding:

MenuSection menu = new MenuSection("La Bella Napoli Menu");

MenuSection pizzas = new MenuSection("Pizzas");
pizzas.add(new MenuSection("Classics")
                .add(new Dish("Margarita", new BigDecimal("8.50")))
                .add(new Dish("Prosciutto", new BigDecimal("9.90"))))
      .add(new MenuSection("Specials")
                .add(new Dish("Tartufo", new BigDecimal("13.50"))));

menu.add(pizzas);
menu.add(new MenuSection("Pastas").add(new Dish("Carbonara", new BigDecimal("10.50"))));

// Uniform treatment: passing a dish, a section, or the whole menu makes no difference.
menu.display(0);
System.out.println("Available dishes: " + menu.countAvailableDishes());

Compare with the first attempt's double loop: here there is not a single if (isSection) in the whole system. The question disappeared because the interface answered it once and for all: every node knows how to operate on itself, and groups know how to delegate. Adding a new operation still has a cost (one more method on the Component and in each class), but adding levels or nodes to the tree costs zero code.

Recursive traversals

Every operation on the tree follows the same mold, worth internalizing because you will write it many times:

  • In the leaf: the base case, a direct answer (a sold-out dish counts 0).
  • In the composite: combine the children's answers (anyMatch, sum, allMatch, concatenate...), calling the same operation on each one. The recursion isn't programmed: it emerges from the structure.

The sequence diagram of countAvailableDishes() on a small menu makes it visible:

sequenceDiagram
    participant App
    participant Menu as menu: MenuSection
    participant Pizzas as pizzas: MenuSection
    participant Marg as margarita: Dish
    participant Water as water: Dish

    App->>Menu: countAvailableDishes()
    Menu->>Pizzas: countAvailableDishes()
    Pizzas->>Marg: countAvailableDishes()
    Marg-->>Pizzas: 1
    Pizzas-->>Menu: 1
    Menu->>Water: countAvailableDishes()
    Water-->>Menu: 1
    Menu-->>App: 2

The app made one call; the structure did the rest. When we get to Iterator and Visitor in module 4, we will come back to this tree to traverse it in more sophisticated ways (mention only for now: Iterator externalizes the traversal; Visitor externalizes the operations).

Combos: when the group is also for sale

The menu groups things in order to display them; combos group things in order to sell them: the "Lunch Combo" (starter + main + dessert, €12.90 instead of the sum), and the "Family Combo" containing two Lunch Combos and a large drink. It is another recursive part-whole —combos inside combos— but with a new operation: the price, computed recursively with a discount:

public class Combo implements MenuComponent {

    private final String name;
    private final BigDecimal discount;              // e.g. 0.15 = 15% off the sum
    private final List<MenuComponent> items = new ArrayList<>();

    public Combo(String name, BigDecimal discount) {
        this.name = name;
        this.discount = discount;
    }

    public Combo add(MenuComponent item) { items.add(item); return this; }

    /** Recursive price: sum of the items' prices, with the discount applied. */
    public BigDecimal getPrice() {
        BigDecimal sum = items.stream()
                .map(Combo::priceOf)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        return sum.multiply(BigDecimal.ONE.subtract(discount))
                  .setScale(2, RoundingMode.HALF_UP);
    }

    private static BigDecimal priceOf(MenuComponent c) {
        if (c instanceof Dish dish)   return dish.getPrice();
        if (c instanceof Combo combo) return combo.getPrice();   // recursion: nested combos
        throw new IllegalStateException("A combo cannot contain sections: " + c.getName());
    }

    /** A combo can only be ordered if ALL its items are available. */
    @Override
    public boolean isAvailable() {
        return items.stream().allMatch(MenuComponent::isAvailable);
    }

    @Override public String getName() { return name; }
    @Override public int countAvailableDishes() {
        return items.stream().mapToInt(MenuComponent::countAvailableDishes).sum();
    }
    @Override public void display(int level) {
        System.out.println("  ".repeat(level) + "★ " + name + " ... €" + getPrice());
        items.forEach(i -> i.display(level + 1));
    }
}

Two nuances with real substance:

  • Notice the change in availability policy: the section uses anyMatch (alive as long as something is left to offer) and the combo uses allMatch (no dessert, no combo). Same recursive mold, different semantics per composite: the pattern provides the structure; the business provides the rules.
  • That instanceof in priceOf is the design's honest seam: getPrice() is not in MenuComponent because a section has no price ("Pizzas" costs nothing: it contains things that cost). We could have hoisted it into the interface and made sections throw an exception... which leads us straight into the pattern's classic debate.

Transparency versus safety

Which operations belong in the Component? The GoF frames the dilemma around the child-management methods (add/remove), and it applies equally to operations like getPrice():

Approach What goes in Component You gain You pay
Transparent Everything, including add/remove Total uniformity: the client never does instanceof or casts Leaves carry absurd methods (dish.add(...)) that must throw UnsupportedOperationException: errors at runtime
Safe Only what is genuinely common; add/remove only in Composite The compiler forbids dish.add(...): errors at compile time The client that builds/modifies the tree needs to know the concrete type (or cast)

Our implementation chose the safe approach (add lives in MenuSection and Combo, not in the interface; getPrice lives in Dish and Combo), and that is the general recommendation in modern Java: compile-time errors are cheaper than runtime ones, and in practice whoever builds the tree almost always knows what it is building (it is built from configuration, a database, or a builder), while whoever traverses it doesn't need to mutate anything. Full transparency has its own territory: frameworks where the client manipulates generic trees without knowing the types (the DOM, file systems). Choose based on who suffers: the one who builds (safe is fine for them, transparent buys them nothing) or the one who traverses (the common subset is all they need)?

When to use it and when not to

Use it when:

  • The domain is a part-whole tree: menus with sections, nested combos, restaurant categories, delivery zones with subzones, permission groups... and you want to operate the same way on the individual and on the group.
  • You spot the double-treatment symptom: the same if (isGroup) ... else ... repeating itself with every new operation.
  • The nesting depth is arbitrary or will grow: parallel lists (List<Section> + List<Dish>) don't scale to that.

Don't use it when:

  • The structure is flat and will stay that way: a list of dishes with no sections doesn't need Component/Leaf/Composite; a List<Dish> is simpler and clearer (KISS).
  • Leaves and groups don't share meaningful operations: if the common interface ends up empty or full of UnsupportedOperationException, the domain is telling you there is no uniform treatment to capture.
  • You need strong constraints on the shape of the tree (at most two levels, specific types per level): Composite's uniformity works against you; sometimes a hand-typed Menu → List<Section> → List<Dish> is exactly what you want.

Relationship to other patterns (mentions only): Decorator is structurally a single-child Composite and often shares its Component interface — we will see it in the next lesson with dish extras; Iterator and Visitor are the patterns for traversal and external operations over these structures; trees are conveniently built with Builder and cloned as templates with Prototype — in fact, module 2's MenuTemplateRegistry stores exactly trees like this one.

Common Mistakes and Tips

  • Returning the live children list. A getChildren() that returns the internal List lets anyone mutate the tree from outside (menu.getChildren().clear()). Return List.copyOf(children) — the same immutability discipline we applied in the Order Builder.
  • Cycles in the "tree". Nothing prevents sectionA.add(sectionB) and sectionB.add(sectionA): the first recursion will swallow it as a StackOverflowError. If the tree is assembled from external data (a restaurant admin panel), validate on add (reject if the new child contains the parent).
  • A parent reference "just in case". Adding getParent() to Component looks innocent, yet it couples every node to its context, complicates moving subtrees, and duplicates the structural truth. Add it only with a real use case (upward navigation) and keep it consistent in add/remove.
  • Putting into Component what isn't common. Every method that only makes sense for one node type pushes toward transparency-with-exceptions. Before hoisting a method into the interface, ask: what does a leaf answer? and a group? If either answer is "no idea", it stays down below.
  • Recursion without a degenerate case. Empty sections: isAvailable() with anyMatch over an empty list returns false (reasonable), but allMatch on an empty Combo returns true (a zero-dish combo, available!). Decide and test the empty cases explicitly.
  • Tip: write your tests against MenuComponent, not against the concrete classes: a test that receives any subtree and checks invariants (count ≥ 0, display doesn't throw) documents the uniform treatment, which is the pattern's promise.

Exercises

Exercise 1: dishes safe for celiacs

Add a countGlutenFree() operation that returns how many gluten-free dishes exist under any node. Assume Dish gains a boolean glutenFree attribute. Write the method in the three classes (Dish, MenuSection, Combo) and state which is the base case and which is the recursive step.

Exercise 2: the impossible combo

The kitchen runs out of the Carbonara, which is the only starter of the Lunch Combo; the Family Combo contains two Lunch Combos and an available drink. Without running any code, reason out what lunchCombo.isAvailable() and familyCombo.isAvailable() return, and why the propagation is automatic.

Exercise 3: transparency with consequences

A colleague proposes hoisting add(MenuComponent) into the MenuComponent interface "so trees can be assembled without casts", making Dish.add throw UnsupportedOperationException. Write (a) an example of code that doesn't compile today but would compile with their proposal and fail at runtime, and (b) a context in which their proposal would nevertheless be the right one.

Solutions

Solution 1:

// In Dish: BASE CASE, direct answer
@Override
public int countGlutenFree() { return (available && glutenFree) ? 1 : 0; }

// In MenuSection: RECURSIVE STEP, combine the children
@Override
public int countGlutenFree() {
    return children.stream().mapToInt(MenuComponent::countGlutenFree).sum();
}

// In Combo: same recursive step over its items
@Override
public int countGlutenFree() {
    return items.stream().mapToInt(MenuComponent::countGlutenFree).sum();
}

And the signature is added to MenuComponent (it is genuinely common: every leaf and every group knows how to answer it). The mold is always the same: leaf = base case, composite = combination of children.

Solution 2: lunchCombo.isAvailable() → the Carbonara returns false, the combo's allMatch finds an unavailable item → false. familyCombo.isAvailable() → its allMatch asks each item; both Lunch Combos return false (by the previous recursion) → false, even though the drink is available. Nobody wrote any "upward" propagation logic: each node applied its local rule (allMatch) and the recursive composition made the global result emerge. One sold-out dish at a leaf switches off combos at any depth.

Solution 3: (a) Today MenuComponent margarita = new Dish(...); margarita.add(somethingElse); doesn't compile (the interface has no add): the error is caught for free. With the proposal, it compiles and blows up at runtime with UnsupportedOperationException — perhaps in production, perhaps in some rare untested case. (b) It would be reasonable in a generic menu editor (an admin panel that manipulates MenuComponent nodes by drag and drop, without knowing concrete types): there, uniformity of manipulation is worth more than static safety, which is exactly the territory of the transparent variant.

Conclusion

Composite turns a part-whole hierarchy into a structure operable through a single interface: leaves that answer directly, groups that combine their children's answers, and clients that make one call without ever asking what they are holding. In PideYa it is now installed in the menu (MenuComponent, Dish, MenuSection) and in nested combos (Combo, with a recursive price and its own availability rule), and the safe variant was chosen over the transparent one —with criteria, not by inertia.

Now look at the dish we just placed on the menu: the Margarita. One customer wants it with double cheese; another, gluten-free; another, large portion with double cheese. Subclasses like DoubleCheeseMargarita, GlutenFreeDoubleCheeseMargarita...? That is another combinatorial explosion, and the answer is not to inherit but to wrap: objects that surround the dish adding price and description, layer by layer, like dressing an onion in reverse. See you in Decorator.

© Copyright 2026. All rights reserved