InterviewKata
Design patterns · 10 of 26 · Fitting objects together

Composite

Composite gives single items and groups the same interface, so code can ask a whole city, a zone or a single station the same question. This page builds Pedal's city as a tree of zones and stations, and shows the bikes that nested loops quietly miss.

1. The pain

Pedal's operations dashboard shows free bikes for the whole city, each zone and each station. London has zones; some zones contain stations directly, and East has a sub-zone, Docklands, with stations of its own. The first version of the dashboard used two nested loops: zones, then the stations in each zone.

The loops quietly skipped Docklands, because a zone inside a zone isn't a station. The fix people reach for is if isinstance(child, Zone): … at every level, in every report that walks the city. Each new level of nesting, or each new report, repeats those checks.

2. The idea in one picture

Ask a company how many people work there. The CEO asks each division head, each division head asks each team lead, each team lead counts their team. Nobody needs to know how deep the organisation goes; everyone answers the same question the same way, and a team of one answers it too.

Composite gives single items (leaves) and groups (composites) the same interface, and lets groups contain either. A Station answers free_bikes() with its own count; a Zone answers by asking each child and adding up. Because a zone's children are just Areas, zones can nest to any depth, and the dashboard calls one method on the city.

The design choice is how much of the interface makes sense for both. free_bikes() does; add_child() doesn't for a station. This page keeps child management on Zone only, so a station can't be asked to hold children.

3. The roles

RoleIts jobIn Pedal
ComponentThe interface for items and groups alike.Area: free_bikes(), stations(), report()
LeafA single item with no children.Station
CompositeA group whose children are components; answers by asking them.Zone
ClientCalls the component interface on whatever it holds.The ops dashboard

4. Before and after

Counting free bikes both ways (Python sketch; the full program is in section 5).

Without the pattern

def city_free_bikes(city):
    total = 0
    for zone in city.zones:
        for child in zone.children:
            if isinstance(child, Station):
                total += child.free
            elif isinstance(child, Zone):
                for s in child.children:   # one more level...
                    total += s.free
    return total

With it

class Zone(Area):
    def free_bikes(self):
        return sum(c.free_bikes()
                   for c in self.children)

class Station(Area):
    def free_bikes(self):
        return self.free

city.free_bikes()          # 23
zone_east.free_bikes()     # 8
The next changeWithout the patternWith it
Docklands gets a sub-zone of its ownAnother loop and another type check in every reportNothing: zones nest to any depth
A new report: stations below 4 freeWrite the nested walk againcity.stations(), then filter
The dashboard shows one stationA separate code path for stationsThe same calls on a Station

The recursion lives in the tree, once, instead of in every piece of code that walks it.

5. The code

Station and Zone share the Area interface; zones hold areas, including other zones. One call reports the whole city, another finds low stations at any depth.

from __future__ import annotations
from abc import ABC, abstractmethod


class Area(ABC):
    """One interface for a single station and for any group of them."""

    def __init__(self, name: str) -> None:
        self.name = name

    @abstractmethod
    def free_bikes(self) -> int: ...

    @abstractmethod
    def stations(self) -> list[Station]: ...

    @abstractmethod
    def report(self, indent: str = "") -> list[str]: ...


class Station(Area):                       # a leaf
    def __init__(self, name: str, free: int) -> None:
        super().__init__(name)
        self.free = free

    def free_bikes(self) -> int:
        return self.free

    def stations(self) -> list[Station]:
        return [self]

    def report(self, indent: str = "") -> list[str]:
        return [f"{indent}{self.name}: {self.free} free"]


class Zone(Area):                          # a composite: holds areas, which may be zones themselves
    def __init__(self, name: str, children: list[Area]) -> None:
        super().__init__(name)
        self.children = children

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

    def stations(self) -> list[Station]:
        return [s for c in self.children for s in c.stations()]

    def report(self, indent: str = "") -> list[str]:
        n = len(self.stations())
        head = f"{indent}{self.name}: {self.free_bikes()} free bikes at {n} station{'s' if n != 1 else ''}"
        return [head] + [line for c in self.children for line in c.report(indent + "  ")]


if __name__ == "__main__":
    city = Zone("London", [
        Zone("Central", [Station("S-12 Holborn", 7), Station("S-14 Bank", 8)]),
        Zone("East", [Station("S-31 Bow", 3), Zone("Docklands", [Station("S-40 Canary Wharf", 5)])]),
    ])
    print("\n".join(city.report()))
    low = [s.name for s in city.stations() if s.free_bikes() < 4]
    print(f"stations below 4 free: {', '.join(low)}")
    one: Area = Station("S-12 Holborn", 7)
    print(f"the same call on one station: {one.name} has {one.free_bikes()} free bikes")
// One interface for a single station and for any group of them.
abstract class Area {
  constructor(readonly name: string) {}
  abstract freeBikes(): number;
  abstract stations(): Station[];
  abstract report(indent?: string): string[];
}

class Station extends Area {   // a leaf
  constructor(name: string, private readonly free: number) { super(name); }
  freeBikes(): number { return this.free; }
  stations(): Station[] { return [this]; }
  report(indent = ""): string[] { return [`${indent}${this.name}: ${this.free} free`]; }
}

class Zone extends Area {   // a composite: holds areas, which may be zones themselves
  constructor(name: string, private readonly children: Area[]) { super(name); }
  freeBikes(): number { return this.children.reduce((sum, c) => sum + c.freeBikes(), 0); }
  stations(): Station[] { return this.children.flatMap((c) => c.stations()); }
  report(indent = ""): string[] {
    const n = this.stations().length;
    const head = `${indent}${this.name}: ${this.freeBikes()} free bikes at ${n} station${n !== 1 ? "s" : ""}`;
    return [head, ...this.children.flatMap((c) => c.report(indent + "  "))];
  }
}

const city = new Zone("London", [
  new Zone("Central", [new Station("S-12 Holborn", 7), new Station("S-14 Bank", 8)]),
  new Zone("East", [new Station("S-31 Bow", 3), new Zone("Docklands", [new Station("S-40 Canary Wharf", 5)])]),
]);
console.log(city.report().join("\n"));
const low = city.stations().filter((s) => s.freeBikes() < 4).map((s) => s.name);
console.log(`stations below 4 free: ${low.join(", ")}`);
const one: Area = new Station("S-12 Holborn", 7);
console.log(`the same call on one station: ${one.name} has ${one.freeBikes()} free bikes`);
import java.util.ArrayList;
import java.util.List;

public class Main {
    // One interface for a single station and for any group of them.
    abstract static class Area {
        final String name;
        Area(String name) { this.name = name; }
        abstract int freeBikes();
        abstract List<Station> stations();
        abstract List<String> report(String indent);
    }

    static class Station extends Area {   // a leaf
        private final int free;
        Station(String name, int free) { super(name); this.free = free; }
        int freeBikes() { return free; }
        List<Station> stations() { return List.of(this); }
        List<String> report(String indent) { return List.of(indent + name + ": " + free + " free"); }
    }

    static class Zone extends Area {   // a composite: holds areas, which may be zones themselves
        private final List<Area> children;
        Zone(String name, List<Area> children) { super(name); this.children = children; }
        int freeBikes() { return children.stream().mapToInt(Area::freeBikes).sum(); }
        List<Station> stations() {
            List<Station> out = new ArrayList<>();
            for (Area c : children) out.addAll(c.stations());
            return out;
        }
        List<String> report(String indent) {
            int n = stations().size();
            List<String> out = new ArrayList<>();
            out.add(indent + name + ": " + freeBikes() + " free bikes at " + n + " station" + (n != 1 ? "s" : ""));
            for (Area c : children) out.addAll(c.report(indent + "  "));
            return out;
        }
    }

    public static void main(String[] args) {
        Zone city = new Zone("London", List.of(
            new Zone("Central", List.of(new Station("S-12 Holborn", 7), new Station("S-14 Bank", 8))),
            new Zone("East", List.of(new Station("S-31 Bow", 3), new Zone("Docklands", List.of(new Station("S-40 Canary Wharf", 5)))))));
        System.out.println(String.join("\n", city.report("")));
        List<String> low = new ArrayList<>();
        for (Station s : city.stations()) if (s.freeBikes() < 4) low.add(s.name);
        System.out.println("stations below 4 free: " + String.join(", ", low));
        Area one = new Station("S-12 Holborn", 7);
        System.out.println("the same call on one station: " + one.name + " has " + one.freeBikes() + " free bikes");
    }
}

Output (the same in all three languages)

London: 23 free bikes at 4 stations
  Central: 15 free bikes at 2 stations
    S-12 Holborn: 7 free
    S-14 Bank: 8 free
  East: 8 free bikes at 2 stations
    S-31 Bow: 3 free
    Docklands: 5 free bikes at 1 station
      S-40 Canary Wharf: 5 free
stations below 4 free: S-31 Bow
the same call on one station: S-12 Holborn has 7 free bikes

6. Try it

Ask any part of the city

Click a zone or station to call free_bikes() on it and watch the call spread down the tree. Compare with the two-loop version.

      7. When not to use it

      What you get

      • One call for any size: a station, a zone or a whole city.
      • Any depth: zones inside zones need no new code.
      • No type checks in clients: the tree does its own walking.
      • New groups are easy: a region of cities is just another composite.

      What you pay

      • A shared interface can be too general: some operations only make sense for groups.
      • Hard to restrict: "zones may only hold stations from one city" needs runtime checks.
      • Hidden cost: one innocent call can walk thousands of nodes.
      • Cycles break it: a zone that contains itself recurses forever.
      If…Use instead
      The structure is always exactly two levelsTwo plain loops are clearer
      You add behaviour around one object, not hold many07 Decorator
      You need several different operations over the treeComposite plus 21 Visitor or 20 Iterator
      The tree is huge and the counts are read constantlyKeep running totals on each zone, updated as bikes move

      8. How it goes wrong

      Child methods on leavesGiving Station an add_child() that throws is a trap at runtime. Keep child management on the composite.
      Type checks creeping backIf a client says if isinstance(area, Zone), an operation is missing from the interface.
      Recomputing big totals on every callSumming 400 stations on each dashboard refresh is fine; summing 400,000 isn't. Cache or keep running counts.
      Allowing cycles or shared childrenA station in two zones is counted twice. Decide whether the structure is a tree and enforce it when adding children.

      9. Remember it and say it

      Memory hook: "Ask down the org chart"Everyone answers the same question: count yourself, or ask your reports and add up.
      How this hook works: what “Ask down the org chart” means

      What the slogan means, phrase by phrase

      “Ask”
      One method on the interface.
      “down the org chart”
      Composites forward to their children.
      “count yourself”
      Leaves answer directly.

      Why this picture fits: Nobody in a company needs to know its depth to give a headcount, and neither does the dashboard.

      The story → the concept

      The companyThe code
      CEOThe London zone
      Division and team leadsCentral, East, Docklands
      A personA Station
      "How many people?"free_bikes()

      Watch it on an example

      1. The dashboard asks London.
      2. London asks Central (15) and East; East asks Bow (3) and Docklands, which asks Canary Wharf (5).
      3. The answers add up on the way back: 23.

      The trap it saves you from

      Nested loops that silently miss anything deeper than they expected.

      It still works when…

      Yes: file systems (folders and files), UI trees (panels and buttons), and organisation charts.

      In an interview or code review

      10. Check yourself

      What do leaves and composites share?

      That's what removes the type checks.

      How does a Zone answer free_bikes()?

      Children may be stations or zones.

      Docklands gets its own sub-zone. What code changes?

      Zones nest to any depth.

      Where should add_child() live?

      Safety over uniformity, in this design.

      A station is added to two zones. What goes wrong?

      Composites assume a tree.

      11. Go deeper

      ResourceWhy
      20 IteratorWalking a composite without knowing its shape.
      21 VisitorAdding new operations over a composite's nodes.
      Python pathlibFolders and files: the composite most people use daily.
      Wikipedia: Composite patternUniformity versus type safety.