- Low Level Design
- /
- Composite Pattern
Course content
An online store sells single products and bundles. The Back to School Kit contains a notebook, a pen pack, and an Art Mini-Kit, and that inner kit is itself a bundle with its own contents and its own discount. The cart, the refund calculator, and the shipping estimator all need one number for whatever they are holding, whether that is a lone stapler or a kit nested three levels deep. The Composite pattern makes that cheap: put single objects and groups of objects behind the same interface, and move the recursion inside the group class, so callers never ask which kind they have.
Three words carry the pattern, and interviewers expect them used precisely. The component is the shared interface both kinds implement: CatalogItem here, declaring what makes sense to ask of one item or a thousand. A leaf is a single object with no children: Product, answering from its own data. A composite is the container: Bundle, holding children typed as the component so it can nest, and answering by combining its children's answers.
The recursion lives inside the tree, not at the call site. That sentence is the pattern. A caller holding a CatalogItem calls price() once; if the item happens to be a bundle, the bundle loops over its children and asks each of them the same question, and any child that is itself a bundle keeps the recursion going. No caller anywhere writes a type check or a tree walk.
The rest of this page builds exactly that catalog. First, the version the pattern rescues you from, with the walk hand-rolled at every call site.
Every service that needs a number from the catalog has to rebuild the same machinery: check the type, unwrap it, recurse by hand, combine the results. The tree walk is the most important calculation in this code and it does not live anywhere; it is re-implemented at every call site that needs it. Pricing has one copy, refunds have one, and shipping will grow one the day someone asks for bundle weights.
Copies drift. The two services above are already out of sync: RefundService was copy-pasted before bundle discounts existed, so it refunds the undiscounted sum. Nothing in the code flags this. Both methods compile, both look plausible in review, and both give identical answers on carts without bundles, which is most carts and all of the original tests.
The type system has been switched off. The children are a list of Object, so the compiler cannot tell a valid catalog from a list with a random string inside (the C++ version needs an empty base class just to get both types into one vector, and still casts its way back out). Every walk rediscovers the types at runtime. And when the business adds a third kind of item, a gift card, a digital download, a warranty, every one of these walks has to be found and extended by hand. The Unknown catalog item exception is a runtime tripwire standing where a compile error should have been.
What this costs in production
The cart total and the refund calculator each carried their own copy of the bundle walk. When nested bundle discounts shipped, the cart copy was updated and the refund copy was not. Every refund on a bundle order quietly paid back the undiscounted price, a few dollars more than the customer had ever been charged. Finance caught the gap in the quarterly reconciliation, forty thousand dollars later. The fix was one line, in a method nobody remembered existed, found by diffing two functions that should never have been two functions.
The catalog is one instance of a shape that repeats wherever objects nest: folders inside folders, UI panels inside panels, org units inside org units, tasks with subtasks. Nobody announces a part-whole tree in a design meeting; you spot one from symptoms like these.
| Smell | What it looks like | Why a composite is the answer |
|---|---|---|
| An if/else on the object's type separating 'one thing' from 'a group' | if (item instanceof Bundle) { recurse } else { return item.getPrice(); } | One interface with the recursion inside the group class removes every check |
| The same tree walk copy-pasted across services | PricingService, RefundService and ShippingService each walk the catalog themselves | Each operation is written once, inside the tree; callers make one call on the root |
| Method pairs that do the same job for singles and groups | priceOf(product) and priceOfBundle(bundle), or add(item) and addBundle(bundle) | When leaf and container share an interface, one method covers both |
| A container that cannot hold its own kind without special handling | Bundle holds Products fine, but a bundle inside a bundle grows a special branch | Composite types its children as the component, so nesting to any depth is free |
| A heterogeneous list typed as Object, or an empty marker base class | List<Object> items; every reader casts its way back to the real types | The component interface is the type that list wanted all along |
The type checks are gone because both kinds of item answer the same question. price() on a product returns its own number; price() on a bundle loops over its children, asks each one, and applies the discount to the sum. The recursion is written once, inside Bundle, which is the only class that even knows children exist. Deep nesting costs nothing: the children are CatalogItems and a Bundle is a CatalogItem, so a kit inside a kit inside a kit is just three constructor calls.
The drift bug is now impossible to write. Pricing and refunds both call item.price(), so there is nothing to copy and nothing to fall out of sync; the discount math has exactly one home. The compiler is back on duty too: the children list is typed, a random string can no longer ride along in a bundle, and a new class that forgets to implement price() fails at compile time instead of tripping a runtime exception in whichever walk meets it first.
New kinds of items are purely additive. A GiftCard with a fixed price, a DigitalDownload that weighs nothing: each is a new leaf implementing CatalogItem, and every existing caller handles it without edits, because there are no walkers left to update. Notice the shape every operation takes: the leaf answers from its own data, the composite answers by combining its children. A second operation, say shipping weight, repeats the same two cases, one honest method per class, and the exercise at the end of this page has you write exactly that.
Two practical questions come up as soon as you use this pattern, and interviewers like both of them.
Who builds the tree? Wiring code at the edge of the system, the same place strategies get chosen and dependencies get injected. A catalog loader reads bundle definitions from the database and assembles Bundle objects; a UI framework inflates a widget tree from a layout file; a parser builds an expression tree from source text. The core of the codebase receives a CatalogItem and calls operations on it; it neither builds trees nor inspects them.
Where does add live? In the code above, add sits on Bundle only, so handing a product to another product is a compile error. That choice is called safety. The alternative puts add and remove on the CatalogItem interface itself, so every node looks identical even to code that edits the tree; leaves then have to throw at runtime when someone calls add on them. That choice is called transparency, and it is the one the original GoF book leans toward. Default to safety. Reach for transparency only when clients genuinely need to edit nodes without knowing their kind, which is rarer than it sounds; a runtime exception from a method the type system happily let you call is a bad trade for symmetry.
With the refactor in hand, it is worth pinning down what Composite is not, because its silhouette, objects holding objects behind a shared interface, belongs to several patterns at once.
Composite vs. Decorator: this is the pair interviewers most like to probe, because the class diagrams are nearly identical and both recurse through wrapped objects. Count the children. A decorator holds exactly one component and adds behaviour around the call on its way through; a composite holds many and combines their answers into one. Intent separates them the rest of the way: Decorator adds responsibilities to a single object (retry this, log this), Composite represents a part-whole tree (this is made of those). If your wrapper starts collecting children it has become a composite; if your bundle always holds one item and adjusts its price, you have written a decorator and should name it accordingly.
Composite vs. a plain tree node: every composite is a tree, but not every tree needs the pattern. A single Node class with an isFile flag and a children list is a legitimate, compact design, and this course's own file system problem solves cleanly that way. The pattern earns its keep when the two kinds genuinely behave differently. The tell is flag checks metastasising: when if (isFile) shows up in size(), then in list(), then in delete(), then in copy(), the class is asking to be split into a leaf and a composite so the branch happens once, at construction time, instead of in every method forever.
Composite vs. nested data: a JSON document, a nested dict, a YAML config tree all nest, and none of them is the Composite pattern. They are data with structure; the pattern is behaviour with structure. If the nodes carry no operations and you process them with external functions and type dispatch, you have a data tree, which is often exactly right (that is how most compilers treat their ASTs). The pattern's claim is specific: the nodes themselves answer operations polymorphically, so new node kinds slot in without touching any external walker.
UI toolkits are the canonical composite, and the same design repeats in every ecosystem. In Swing, Component is the component interface, JButton and JLabel are leaves, and Container is the composite: panel.add(button) builds the tree, and because Container extends Component, panels nest inside panels indefinitely. Painting and layout are recursive walks: frame.pack() sizes the window by asking each child its preferred size, which asks its children in turn. Android's View and ViewGroup are the identical split (measure, layout and draw each recurse down the tree), the browser DOM is one more (element.appendChild accepts a text node or a subtree of ten thousand elements with equal indifference), and every Qt window is a tree of QWidgets where show() and setEnabled() cascade to children.
Document and data trees follow. In Python, xml.etree.ElementTree gives you Element objects whose children are Elements; findall and iter walk any subtree, and serialising a node serialises everything under it. Jackson's JsonNode in Java is a textbook composite: ObjectNode and ArrayNode hold children, TextNode and IntNode are leaves, and toString() on any node prints its whole subtree. An SVG <g> group is a composite whose transform applies to every child, groups included.
Expression trees are the version you have most likely written by hand. A calculator or rule engine defines an Expr interface with an evaluate() method, Literal as the leaf, and Add and Multiply (or And and Or) as composites holding sub-expressions; evaluating the root evaluates the tree. Database query planners are the industrial version: a join node holds child plan nodes, and cost estimation and execution both recurse. Spreadsheet formula engines, feature-flag rule trees, and firewall policy groups repeat the shape.
C++ puts the pattern at the centre of graphics. A game engine's scene graph is a tree of nodes where a tank's turret is a child of its hull; a node's world transform multiplies its parent's, so moving the hull moves everything, and drawing the root draws the world. Qt's QObject parent system is composite with ownership: deleting a widget recursively deletes its children, which is why closing a window does not leak its buttons. And the shape appears with no UI at all in nested security groups: a directory group contains users and other groups, and answering whether a user is covered is a walk over a composite tree.
| Pros | Cons |
|---|---|
| Callers treat one object and a whole tree identically; the type checks and hand-rolled walks disappear | Adding a new operation touches every class in the hierarchy; the interface change fans out to all leaves and composites |
| Each operation's recursion is written once, inside the composite, instead of once per calling service | A too-uniform interface invites methods that make no sense on leaves (add on a Product): the transparency vs. safety trade has no free option |
| New node kinds are additive: implement the interface and every existing traversal just works | The type system stops restricting what a composite may contain; nothing prevents a Wheel inside an Engine without your own runtime checks |
| Arbitrary nesting comes free because children are typed as the component | Nothing stops a node being added to two parents, or to itself; a cycle turns price() into infinite recursion unless mutation paths guard against it |
| Whole-tree operations collapse to one call on the root | Deep or repeated traversals cost real time; caching aggregates in the composite means invalidating them on every add or remove |
Composite pays off when the structure genuinely nests and both kinds appear in the same positions. If the collection is flat, a cart that holds products and nothing else, the honest type is a list of products; wrapping it in a pattern adds indirection and saves nothing. The same goes for exactly-one-level containment that will never nest: an order with line items is a class holding a list, not a part-whole tree, and pretending otherwise buys machinery without a problem.
Be suspicious when the shared interface will not stay shared. If leaves and containers have almost no operations in common, so the component interface is either nearly empty or full of methods half the classes cannot honestly implement, the uniformity is fake and two separate types tell the truth. The single-class tree with a flag, from the cousins section, remains the right call when both kinds share nearly all behaviour and the set of kinds is fixed.
Finally, check the structure is actually a tree. Catalogs drift toward graphs: the same product object referenced by two bundles is harmless for pricing, but the moment an operation depends on a node's parent (remove me, compute my path, invalidate my ancestors' caches) shared children and multiple parents break the pattern's assumptions. If your structure is a DAG or a general graph, model it as one; composite's recursion assumes each node has one home.
Three flavours of question recur. The first is design: 'design a file system', 'design a menu with nested submenus', 'model an org chart with team-level salary rollups', 'nested comment threads', 'a graphics editor where groups of shapes move together'. The expected first move is to name the part-whole structure, define the component interface, and write one leaf and one composite with the recursion inside the composite. The standard follow-up is whether add belongs on the interface or on the composite; the safety-versus-transparency trade from earlier on this page is the answer, and having a default (safety) with a reason is what scores. This course's file system problem is the classic prompt in exercise form, and it is also a chance to say something sharper: its compact single-class solution is fine, and you reach for real leaf and composite classes when the flag checks start spreading.
The second flavour is comparison, and one contrast dominates: Composite vs. Decorator. Answer with cardinality and intent in one breath, a decorator wraps exactly one component to add behaviour, a composite holds many children to represent a whole, and you are done. The name not yet mentioned on this page is Visitor: when the interviewer pushes on 'so adding a new operation means touching every class?', the escape hatch is the Visitor pattern, which moves operations into their own objects at the cost of double dispatch and a fixed set of node kinds. Composite optimises for new kinds of nodes, Visitor for new operations over fixed kinds; naming that axis is a senior-level answer.
The third flavour is implementation depth. Can a bundle contain itself? Nothing in the types prevents it, and the result is infinite recursion, so mutation paths need a cycle guard: walk the ancestor chain, or track membership before adding. What about caching? A composite that memoises its subtotal must invalidate ancestors on every add or remove, which is why cached aggregates usually arrive together with parent pointers and dirty flags. How deep can the tree go? Recursive walks overflow the stack on degenerate ten-thousand-level trees, and the fix is an explicit stack, the same transformation as for any recursion. Each of these is a one-sentence answer, and volunteering one before being asked signals you have actually shipped a tree.
The companion exercise gives you the CatalogItem interface and a finished Product leaf, and asks you to implement Bundle: store children of the component type, sum their prices and apply the bundle's percentage discount, and sum their weights with no discount at all. The validator prices flat and nested bundles (including an empty one), checks the discount applies at every nesting level, and defines a brand-new catalog item type of its own to prove your bundle handles children strictly through the interface. Starter code and validators ship in Java, Python, and C++, so you can practise in the language you interview in.
Open the exercise: Composite: Price a Catalog of Nested Product Bundles.