Composite

Part of the free Low-Level Design course on LogicWiz, in the chapter: Structural patterns. The class-diagram exercises run in the app.

Composite: bundles inside bundles

"The Breakfast Box has an Egg Combo inside it, and the Egg Combo is a bundle too. My price method now checks instanceof at every level."

Arjun, at Priya's desk with his laptop


The problem: things inside things

SwiftKart sells bundles: several items sold together, often at a discount. The "Breakfast Box" holds bread, milk and an "Egg Combo". The Egg Combo is itself a bundle: six eggs and butter, at 10% off. So a bundle can hold products, and it can also hold other bundles.

The catalogue has the same shape. The "Dairy" category holds "Milk", which holds "Toned milk", which holds the actual milk packets. Any category can hold products and other categories, to any depth.

Here is Arjun's first Bundle. Prices are in paise (one rupee is 100 paise).

public class Bundle {
    private final String name;
    private final int discountPercent;
    private final List<Object> items = new ArrayList<>(); // products or bundles

    // constructor and add method left out

    public long pricePaise() {
        long total = 0;
        for (Object item : items) {
            if (item instanceof Product p) {
                total += p.pricePaise();
            } else if (item instanceof Bundle b) {
                total += b.pricePaise();
            }
        }
        return total - total * discountPercent / 100;
    }
}

Here is what hurts:

  • The list accepts anything. A List<Object> takes a String or a Rider without complaint. The compiler cannot stop it, and the loop skips it without a word.
  • Every operation repeats the check. inStock() and weightGrams() need the same instanceof pair. So does the cart, when it shows a line.
  • A new kind of thing touches everything. If SwiftKart puts gift cards in bundles, every one of those if chains needs a third branch.

At every step, the code asks "is this one thing, or a group of things?" That question is the problem.

The idea: folders inside folders

Your laptop already solves this. A folder can hold files, and it can hold other folders. Right-click a file and ask for its size: you get a number. Right-click a folder and ask for its size: you also get a number. The folder works it out by asking each thing inside for its size and adding the answers. Those things answer the same way, whether they are files or folders. You never think "if file, else if folder".

That is the composite pattern: a single item and a group of items share one interface, so callers treat them the same. The roles have names:

  • The component is the shared interface: Sellable, which has pricePaise().
  • A leaf is a single item with no children: a Product.
  • A composite is a group. It implements the component and holds a list of components, which can be leaves or other composites: a Bundle.

A group answers a question by asking each child the same question and combining the answers. When a method calls the same method on smaller parts until it reaches the simplest case, that is recursion. Here, the simplest case is a leaf, which answers directly.

The folder picture stops working in two places. A folder's size is always the plain sum of its contents, but a bundle can add its own rule on top, such as 10% off. And a folder cannot sit inside itself on disk, so you never worry about loops there. In code, nothing stops a bundle from ending up inside itself. You will see below why that matters.

The structure

classDiagram
  class Sellable {
    <<interface>>
    +pricePaise() long
    +inStock() boolean
  }
  class Bundle {
    +add(child) void
  }
  Cart --> Sellable
  Sellable <|.. Product
  Sellable <|.. Bundle
  Bundle o-- Sellable : children

The arrow that makes this a composite is the hollow diamond from Bundle back to Sellable. A bundle holds children of the interface type, not of Product. That one choice allows bundles inside bundles. Notice also that add appears on Bundle only. The cart points at Sellable, so it never needs to know which kind it holds.

Build it step by step

Step 1: the shared interface, and the leaf. A record is a short Java class for data that does not change. Java writes the name() and pricePaise() methods for it, and they fulfil the interface.

public interface Sellable {
    String name();
    long pricePaise();
    boolean inStock();
}

public record Product(String name, long pricePaise, int stock) implements Sellable {
    public boolean inStock() {
        return stock > 0;
    }
}

Step 2: the composite.

public class Bundle implements Sellable {
    private final String name;
    private final int discountPercent;
    private final List<Sellable> children = new ArrayList<>();

    public Bundle(String name, int discountPercent) {
        this.name = name;
        this.discountPercent = discountPercent;
    }

    public void add(Sellable child) {
        if (child == this) throw new IllegalArgumentException("A bundle cannot contain itself");
        children.add(child);
    }

    public String name() { return name; }

    public long pricePaise() {
        long total = 0;
        for (Sellable child : children) total += child.pricePaise();
        return total - total * discountPercent / 100;
    }

    public boolean inStock() { return children.stream().allMatch(Sellable::inStock); }
}

Look at pricePaise(). The loop does not ask what each child is. A product returns its price. A bundle runs this same method on its own children and returns its discounted total. inStock() works the same way: a bundle is in stock only if everything inside it is in stock.

Step 3: build a tree, and ask it.

Bundle eggCombo = new Bundle("Egg Combo", 10);
eggCombo.add(new Product("Eggs, 6", 9000, 40));
eggCombo.add(new Product("Butter 100 g", 6000, 12));

Bundle breakfast = new Bundle("Breakfast Box", 0);
breakfast.add(new Product("Bread", 4000, 25));
breakfast.add(new Product("Toned milk 1 L", 3000, 60));
breakfast.add(eggCombo);

long price = breakfast.pricePaise(); // 4000 + 3000 + 13500 = 20500

Trace it. The breakfast box asks bread (4000), milk (3000) and the Egg Combo. The Egg Combo asks eggs (9000) and butter (6000), gets 15000, takes 10% off and returns 13500. The box adds everything up to 20500. The cart calls pricePaise() on whatever sits in a line, product or bundle, and never checks which.

The category tree is the same pattern with different names. Category is the composite and Product is the leaf. A product's productCount() returns 1, and a category's returns the sum of its children's counts. You will draw it in the exercise below.

The guard in add stops a bundle from holding itself directly. A loop through two bundles (A holds B, and B holds A) would still make pricePaise() call itself forever and crash with a StackOverflowError. If bundles come from an admin screen, check for loops when saving them.

Where does add belong? Arjun suggested moving it to the interface. "Then every node has the same methods, and callers never need to know which kind they hold. Product.add can throw an exception." This is a real choice, and the 1994 Design Patterns catalogue discusses both options. This course keeps add on the group, for a reason you already know.

The code that adds children is building a tree, so it already knows it holds a bundle. Putting add on Sellable makes a promise that Product cannot keep. That breaks the Liskov substitution principle from the SOLID chapter: any implementation must work wherever the interface is expected, with no surprises. A Product.add that throws is a surprise. One that ignores the child is worse, because the bug hides. Keep what every node can really do (name, price, stock) on the interface. Keep what only groups can do (add, remove, list children) on the group.

When to use composite, and when not

Use composite when your data is a tree of any depth, and most operations work the same on one item and on many. Prices, counts, "hide everything under this category" and search all fit.

Do not use it when:

  • the list is flat. A cart holds cart lines, and a line never holds another line. A List<CartLine> is enough.
  • leaves and groups need very different operations. The interface fills up with methods that fit only one side, and the implementations start to throw. That is the Liskov problem again.

Composite has costs:

  • It is hard to limit what goes inside. If a combo may hold only products, the type Sellable does not say so. You need a check at runtime.
  • Deep trees repeat work. Each call walks the whole tree. For a category tree with 50,000 products, store each category's count and update it when the tree changes.
  • Loops need a guard, as you saw above.

The look-alike is the decorator from the last lesson. Both hold the shared interface. A decorator holds exactly one inner object and adds to its answer. A composite holds many children and combines their answers. You can think of a decorator as a group with exactly one child.

Composite appears in many interview problems:

  • a file system, with directories and files;
  • a menu with submenus, or an organisation chart of managers and staff;
  • an expression tree for a calculator, where Add holds two smaller expressions;
  • the rate limiter problem in the library, which combines several limiters into one.

Try it: the category tree

The starter is the catalogue's category tree, with separate lists for products and subcategories. Give products and categories one interface, and let a category hold children of that interface.

📌 Summary: Composite gives a single item and a group one interface. The group holds children of that interface and answers by asking each child, which is recursion. Keep add on the group only.

What Arjun learns next

So far, Arjun has fitted, simplified, wrapped and nested objects. Next comes the last structural pattern: an object with the same interface that stands in front of a slow or sensitive one. That is the proxy.

Check your understanding

  1. In the bundle composite, should Product get an add(child) method so every node has the same methods?
  2. The Breakfast Box (no discount) holds bread at 4000 paise, milk at 3000, and an Egg Combo of eggs at 9000 and butter at 6000, with 10% off. What does pricePaise() return?
  3. A cart holds cart lines, and a cart line never holds another line. Should the cart use the composite pattern?
  4. Through an admin screen, bundle A ends up inside bundle B, and B inside A. What happens when the cart asks A for its price?