codejump

AcademyDesign Patterns

Composite pattern

Treating a single object and a group of them through one interface, the cost every leaf pays for methods it may never use, and a leaf that accepts children it cannot hold.

6 min readUpdated

3 exercises

Waiting at the end of this page.

Composite is the pattern for trees, and it earns the name literally: every node in the tree, leaf or branch, is built to answer the exact same call.

A Composite gives a leaf object and a collection of them the same interface, so calling code never has to check which one it's holding. A single file or a whole folder answer the same getSize() call, and a folder's answer is just the sum of whatever its children answer, whether those children are files or folders themselves.

The problem it solves

You are computing the size of a folder. A folder holds files, and folders hold more folders. Without a shared interface, the code has to check what each item is:

TypeScript
function totalSize(item: File | Folder): number {
  if (item instanceof File) return item.size;
  return item.children.reduce((sum, child) => sum + totalSize(child), 0);
}

That instanceof check is small here, but it multiplies.

  • Every operation on the tree (size, search, render, delete) repeats the same "leaf or branch?" branch.
  • Adding a new kind of node means editing every one of those functions.
  • Callers cannot treat a single item and a group the same way, even when they want to.

The fix is to make files and folders answer the same call, so the branching lives inside the objects instead of around them.

Structure

File and Folder both implement Component. A Folder holds many Components, which may be files or other folders
File and Folder both implement Component. A Folder holds many Components, which may be files or other folders

Three parts:

  • The component interface (getSize()): what every node can do.
  • The leaf (File): does the work directly.
  • The composite (Folder): holds children behind the same interface and delegates to them.
class File {
  constructor(size) { this.size = size; }
  getSize() { return this.size; }
}
class Folder {
  constructor() { this.children = []; }
  add(child) { this.children.push(child); }
  getSize() { return this.children.reduce((sum, c) => sum + c.getSize(), 0); }
}

// const root = new Folder();
// root.add(new File(100));
// root.add(new File(250));
// const sub = new Folder();
// sub.add(new File(40));
// root.add(sub);
// root.getSize() → 390

root.getSize() never needs to know how deep the tree goes. It delegates to its own children's getSize(), and each of those does the same, however many folders down.

Each Folder only asks its direct children, so the total is computed by recursion with no special case for depth
Each Folder only asks its direct children, so the total is computed by recursion with no special case for depth

That recursive delegation, not a special case for "am I a leaf or a branch", is the entire pattern.

Where it is used in real systems

Use caseLeafComposite
File systemsFileFolder
UI component treesButton, textPanel, page, layout
The DOMText nodeElement
MenusMenu itemSubmenu
Org chartsEmployeeTeam or department
PermissionsA single permissionA role made of permissions
ExpressionsA numberAn operation with sub-expressions
CommentsA replyA thread of replies

Any time the data is a tree and you want to operate on "this node and everything below it", you are looking at Composite.

Composites you already use

  • The DOM. A div and a text node are both Nodes. element.remove(), textContent and querySelectorAll work the same at any depth.
  • React and Vue component trees. A component renders children that are themselves components. Rendering the root renders the tree.
  • JSON. A value is a primitive (leaf) or an object or array of values (composite).
  • Angular and Vue routers. Nested route configurations are a tree walked through one interface.
  • Abstract syntax trees. Compilers and linters (ESLint, Babel) visit a tree of nodes, all of which share a node interface.

The cost

Composite makes it easy to add a new kind of node, but it has to answer one uncomfortable question: which operations belong on the shared interface?

  • add(child) makes sense on a folder but not on a file. Put it on the interface and files carry a method that must throw or do nothing. Keep it only on the composite and callers lose uniform treatment when building the tree.
  • A very general interface can make types too loose to be useful.

There is no perfect answer, which is why this comes up in interviews. Keep the shared interface to the operations that truly make sense for every node.

When not to use it

  • The data is not a tree. A flat list does not need Composite. A graph with cycles needs a different tool, because recursive delegation would loop forever.
  • Leaves and branches behave nothing alike. If callers always need to know which one they hold, forcing one interface hides real differences.
  • Shallow and fixed structure. Two levels that will never change are simpler with plain arrays.
  • Performance-critical traversal. Deep recursion can overflow the call stack, and virtual calls on every node cost something.

What interviewers ask

  • "What is Composite?" Treating a single object and a group of objects through one interface, usually to model a tree.
  • "Give an example." File system, DOM, or a UI component tree.
  • "Where does `add()` belong?" The classic trade-off: on the interface for transparency, or only on the composite for safety.
  • "How do you avoid infinite recursion?" Composite assumes a tree. Guard against a node being added under itself, or use a visited set.
  • "Composite versus Decorator?" Both use recursive composition. Decorator wraps one object to add behavior. Composite holds many to represent a whole.

The mistake to avoid

A Composite where a leaf silently accepts children it can never hold. If File.add() is allowed and quietly ignores its argument, the data is lost and nothing tells you. Either throw a clear error, or leave add() off the leaf type so the compiler stops the call.

The mechanism above, nodes holding nodes of the same type, is the same in JavaScript, Python or Java. Only the syntax changes.

Now practice it

1 challenge · 2 quizzes

Reading this page is the cheap half. These are the exercises that make you use it.