Java design pattern deep dive
Composite Pattern in Java: 100+ questions and answers for tree-shaped object structures.
Learn how to treat single objects and whole subtrees uniformly, with worked examples across file systems, UI component trees, org charts, and menu structures — plus the cycle-safety and transparent-versus-safe design decisions that come up in real code review.
How to think about this pattern
Composite is really an agreement: every node in a tree, whether it has zero children or a thousand, answers to the same interface. The hard decisions are what that interface should expose and how far "uniform" should go.
| Approach | Optimizes for | Trade-off |
|---|---|---|
| Transparent Composite | Uniform client code — never cast, never branch on node type. | Leaf must implement structural methods it cannot honor, usually by throwing UnsupportedOperationException. |
| Safe Composite | Type safety — a Leaf simply has no add() method to misuse. | Client code that builds the tree needs an instanceof check or a cast to reach Composite-only methods. |
| Decorator | Adding responsibilities to a single object dynamically through a linear wrapper chain. | Not built for whole-part trees; each decorator wraps exactly one inner component, not a list of children. |
| Plain nested collections (no pattern) | Quick one-off scripts over ad hoc List/Map nesting. | No shared interface — every new operation and every new node shape means touching call sites by hand. |
Categories
Questions and answers
Each answer favors the decision and the trade-off over syntax alone. Most include runnable-shape Java; the rest use tables and callouts where a comparison communicates faster than code.
Core concepts & structure
1. What problem does the Composite pattern actually solve?
It solves the "one thing versus a group of things" problem: client code wants to call the same operation whether it is holding a single object or an entire subtree of objects, without an if (isGroup) check at every call site. Composite defines one interface that both a single leaf and an arbitrarily deep group implement.
2. What are the three core roles in Composite, and what does each own?
Component declares the operations common to both. Leaf implements Component with no children — it is the recursion's base case. Composite implements Component and additionally holds a collection of child Components, implementing each operation by delegating to every child.
interface FileSystemComponent {
long size();
}
final class FileLeaf implements FileSystemComponent {
private final String name;
private final long bytes;
FileLeaf(String name, long bytes) {
this.name = name;
this.bytes = bytes;
}
public long size() {
return bytes;
}
}
final class Directory implements FileSystemComponent {
private final String name;
private final List children = new ArrayList<>();
Directory(String name) {
this.name = name;
}
void add(FileSystemComponent child) {
children.add(child);
}
public long size() {
long total = 0;
for (FileSystemComponent child : children) {
total += child.size();
}
return total;
}
}
3. Why does Composite favor object composition over class inheritance?
Inheritance would need a class for every possible shape of tree node, and it cannot express "has children of the same type as me" cleanly. Composition lets a Composite simply hold a List<Component> — any mix of Leaf and Composite instances — so the tree shape is a runtime structure, not a compile-time class hierarchy. This is also why Composite is usually taught alongside Decorator: both replace inheritance explosion with a held reference.
4. How do you define the common Component interface so both files and directories fit it?
Put only the operations that make sense for both leaf and group behind the interface — typically read-only or whole-tree operations such as size(), print(), or accept(Visitor). Structural methods like add() are a separate design decision, covered later in the transparent-versus-safe section.
interface FileSystemComponent {
long size();
void print(String indent);
}
5. How does a Composite implement an operation so it covers the whole subtree?
The Composite's method body does two things: something local (optional), then a loop that calls the same method on every child. Because each child might itself be a Composite, the call naturally recurses until it bottoms out at Leaf nodes, which do the real work and return.
public void print(String indent) {
System.out.println(indent + name + "/");
for (FileSystemComponent child : children) {
child.print(indent + " ");
}
}
6. Should Composite operations return aggregated values, or just perform side-effecting actions?
Both are common and often coexist on the same interface. Aggregating operations (size(), totalSalary()) return a value built by combining each child's result. Action operations (print(), render(), disable()) just recurse for effect. The shape of the base case at Leaf tells you which kind you are writing: an aggregator's Leaf returns a concrete value; an action's Leaf just performs its own step.
7. How do you add and remove children from a Composite without leaking the internal list?
Never return the live backing list from a getter. Expose add/remove methods that mutate the internal collection, and if callers need to inspect children, return an unmodifiable view.
final class Directory implements FileSystemComponent {
private final List children = new ArrayList<>();
void add(FileSystemComponent child) {
children.add(Objects.requireNonNull(child));
}
void remove(FileSystemComponent child) {
children.remove(child);
}
List children() {
return Collections.unmodifiableList(children);
}
// size(), print() as before
}
8. How is Composite different from just modeling a tree as nested generic JSON/Map structures?
A generic tree of Map<String,Object> is flexible but untyped: every consumer has to know the shape by convention and cast defensively. Composite gives each node kind a real Java type with its own invariants (a Leaf cannot have children; a Directory always has a name), and the compiler enforces the shared interface. Use generic structures for quick data interchange; use Composite when the tree has real behavior attached to it.
9. Can a Composite node be a child of another Composite, and how deep can that go?
Yes — that nesting is the entire point of the pattern. A Directory can contain FileLeaf children and other Directory children, which can contain further Directories, to whatever depth the data requires. The recursion in each operation does not care about depth; it only cares that every child, at every level, implements the same Component interface.
Directory root = new Directory("root");
Directory src = new Directory("src");
Directory main = new Directory("main");
main.add(new FileLeaf("App.java", 2_048));
src.add(main);
root.add(src);
root.add(new FileLeaf("README.md", 512));
System.out.println(root.size()); // 2560, three levels deep
10. What code-review smells suggest Composite would simplify a design?
Watch for repeated instanceof chains that branch on "is this a group or a single item," parallel fields like List<Leaf> leaves and List<Group> groups that always get iterated together, and recursive helper methods duplicated across otherwise-unrelated classes because there is no shared interface to hang them on.
File system trees
11. Model a file system where a File is a leaf and a Directory is a composite.
Give both a common FileSystemComponent interface, keep FileLeaf immutable, and let Directory hold children plus its own aggregating logic.
interface FileSystemComponent {
String name();
long size();
}
final class FileLeaf implements FileSystemComponent {
private final String name;
private final long bytes;
FileLeaf(String name, long bytes) { this.name = name; this.bytes = bytes; }
public String name() { return name; }
public long size() { return bytes; }
}
final class Directory implements FileSystemComponent {
private final String name;
private final List children = new ArrayList<>();
Directory(String name) { this.name = name; }
public String name() { return name; }
void add(FileSystemComponent c) { children.add(c); }
public long size() {
return children.stream().mapToLong(FileSystemComponent::size).sum();
}
}
12. How do you compute the total size of a directory, including every nested subdirectory?
size() is already recursive by construction: a Directory sums its children's size(), and any child that is itself a Directory repeats the same summation one level down. No caller needs to know how deep the tree goes.
long totalBytes = root.size(); // walks the entire subtree
13. How do you print an indented directory tree similar to the Unix tree command?
Add a print(String indent) method to the Component interface. Leaf prints its own line; Directory prints its own line then recurses into each child with one more level of indent.
void print(String indent) {
System.out.println(indent + name + " (" + size() + " bytes)");
}
// Directory override
void print(String indent) {
System.out.println(indent + name + "/");
for (FileSystemComponent child : children) {
child.print(indent + " ");
}
}
14. How do you count files versus directories across the whole tree?
Add two aggregating operations, or one that returns a small record. Each Leaf contributes one file; each Directory contributes one directory plus whatever its children contribute.
record Counts(int files, int directories) {
Counts plus(Counts other) {
return new Counts(files + other.files, directories + other.directories);
}
}
// Leaf: return new Counts(1, 0);
// Directory:
Counts counts() {
Counts total = new Counts(0, 1);
for (FileSystemComponent child : children) {
total = total.plus(child.counts());
}
return total;
}
15. How do you find the largest file anywhere in a nested directory structure?
Thread an "accumulator" through the recursion, or have each node return its own best candidate and let the parent pick the larger of what its children returned.
Optional largestFile() {
Optional best = Optional.empty();
for (FileSystemComponent child : children) {
Optional candidate = (child instanceof FileLeaf leaf)
? Optional.of(leaf)
: ((Directory) child).largestFile();
if (candidate.isPresent() && (best.isEmpty() || candidate.get().size() > best.get().size())) {
best = candidate;
}
}
return best;
}
16. How do you implement a search-by-name that works across an arbitrarily nested tree?
Leaf checks itself and returns a match or nothing; Directory checks itself, then asks each child, stopping early once a match is found.
Optional findByName(String target) {
if (name.equals(target)) return Optional.of(this);
for (FileSystemComponent child : children) {
Optional found = (child instanceof Directory dir)
? dir.findByName(target)
: (child.name().equals(target) ? Optional.of(child) : Optional.empty());
if (found.isPresent()) return found;
}
return Optional.empty();
}
17. How do you delete a directory and everything inside it using the Composite structure?
Add a delete() operation: Leaf deletes its own backing file; Directory recursively deletes every child first, then removes itself. Order matters — children must go before the parent directory can be removed on most real file systems.
public void delete() throws IOException {
for (FileSystemComponent child : new ArrayList<>(children)) {
child.delete();
}
Files.deleteIfExists(path);
}
delete() also removes each child from its parent's collection, or you will get a ConcurrentModificationException.18. How do you copy an entire directory tree while preserving its structure?
Give each node a copy() method that returns a new node of the same kind. Directory's copy creates a new empty Directory, then recursively copies and adds each child's copy.
public FileSystemComponent copy() {
Directory clone = new Directory(name);
for (FileSystemComponent child : children) {
clone.add(child.copy());
}
return clone;
}
// Leaf.copy() just returns new FileLeaf(name, bytes)
19. How do you compute a permission such as "fully writable" that must hold for every node in a subtree?
This is a boolean-AND aggregation: a Directory is writable only if it is writable itself and every child reports writable. Short-circuiting with allMatch avoids visiting the whole tree once a false is found.
public boolean isFullyWritable() {
return writable && children.stream().allMatch(FileSystemComponent::isFullyWritable);
}
20. How would you filter a tree to show only files matching an extension, while preserving directory structure in the result?
Build a new, parallel tree rather than mutating the original: each Directory recursively filters its children, keeping only matching Leaf nodes and only sub-Directories that still have something left inside after filtering.
public Optional filterByExtension(String ext) {
if (this instanceof FileLeaf leaf) {
return leaf.name().endsWith(ext) ? Optional.of(leaf) : Optional.empty();
}
Directory filtered = new Directory(name);
for (FileSystemComponent child : children) {
child.filterByExtension(ext).ifPresent(filtered::add);
}
return filtered.children.isEmpty() ? Optional.empty() : Optional.of(filtered);
}
UI component trees
21. Model a UI widget tree where a Button is a leaf and a Panel is a composite.
Both implement UiComponent. Panel adds a child list and delegates every operation, exactly like Directory did for the file system.
interface UiComponent {
void render(int depth);
}
final class Button implements UiComponent {
private final String label;
Button(String label) { this.label = label; }
public void render(int depth) {
System.out.println(" ".repeat(depth) + "[Button: " + label + "]");
}
}
final class Panel implements UiComponent {
private final String name;
private final List children = new ArrayList<>();
Panel(String name) { this.name = name; }
void add(UiComponent child) { children.add(child); }
public void render(int depth) {
System.out.println(" ".repeat(depth) + "");
for (UiComponent child : children) {
child.render(depth + 1);
}
}
}
22. How does render() draw an entire nested UI without the caller knowing the tree shape?
The caller invokes render() once on the root Panel. Every Panel along the way recurses into its own children, so the whole visual tree is drawn by one top-level call regardless of nesting depth.
Panel window = new Panel("Window");
Panel toolbar = new Panel("Toolbar");
toolbar.add(new Button("Save"));
toolbar.add(new Button("Open"));
window.add(toolbar);
window.add(new Button("Close"));
window.render(0);
23. How do you propagate a click event down through a component tree, or bubble it up?
Dispatch-down: the root receives the raw event (like a screen coordinate) and each Panel forwards it only to the child whose bounds contain the point, until a Leaf claims it. Bubble-up: once a Leaf handles an event, it walks its stored parent reference upward, letting each ancestor Panel react (for example, a Panel that shows a "dirty" indicator when any descendant changes).
public void dispatch(Point p) {
if (!bounds.contains(p)) return;
for (UiComponent child : children) {
child.dispatch(p);
}
}
24. How do you compute a container's total layout size from its children?
This is another aggregation: a Panel's preferred size is derived from combining its children's sizes according to a layout rule (stack, row, grid), while a Leaf simply reports its own fixed or intrinsic size.
public Dimension preferredSize() {
int width = 0, height = 0;
for (UiComponent child : children) {
Dimension d = child.preferredSize();
width = Math.max(width, d.width());
height += d.height();
}
return new Dimension(width, height);
}
25. How do you enable or disable an entire UI subtree with a single call?
Add setEnabled(boolean) to the Component interface. Leaf just flips its own flag; Composite flips its own flag and forwards the call to every child, so one call at the Panel root cascades to every button and field beneath it.
public void setEnabled(boolean enabled) {
this.enabled = enabled;
for (UiComponent child : children) {
child.setEnabled(enabled);
}
}
26. How do you find a component by id inside a deep UI tree, the way Swing or JavaFX lookups work?
Same shape as the file-system name search: check self, then ask each child, short-circuiting on the first match. Keep an id field on the shared interface so both Leaf and Composite can be checked identically.
public Optional findById(String id) {
if (id.equals(this.id)) return Optional.of(this);
for (UiComponent child : children) {
Optional found = child.findById(id);
if (found.isPresent()) return found;
}
return Optional.empty();
}
27. How do you apply a theme to every component in a Composite UI tree in one call?
Add applyTheme(Theme theme) as an operation. Each Leaf restyles itself using the theme's colors/fonts; each Composite restyles itself, then propagates the same theme object to every child so the whole subtree updates consistently.
public void applyTheme(Theme theme) {
this.background = theme.panelBackground();
for (UiComponent child : children) {
child.applyTheme(theme);
}
}
28. How does hiding a Panel also hide every descendant without looping over them individually at the call site?
Two valid designs: (1) propagate a setVisible(false) call recursively like setEnabled above, or (2) make isVisible() check the parent chain — a Leaf is only actually visible if it, and every ancestor Panel up to the root, are visible. Option 2 avoids rewriting every descendant's flag but requires each node to know its parent.
29. How do you compute a "dirty" repaint region across a component tree?
Treat it as an aggregation of rectangles: each dirty Leaf contributes its own bounds; a Composite unions the dirty regions of its children (skipping clean subtrees entirely as an optimization) and reports the combined rectangle upward.
public Optional dirtyRegion() {
Rectangle union = null;
for (UiComponent child : children) {
Optional r = child.dirtyRegion();
if (r.isPresent()) {
union = (union == null) ? r.get() : union.union(r.get());
}
}
return Optional.ofNullable(union);
}
30. How would you structure a unit test that toggling a container's visibility affects nested leaf components?
Build a small three-level fixture (Panel containing a Panel containing a Button), call setVisible(false) on the outer Panel, then assert the innermost Button reports not visible — this proves propagation actually reaches the leaf rather than stopping at the middle layer.
@Test
void hidingOuterPanelHidesNestedButton() {
Panel outer = new Panel("outer");
Panel inner = new Panel("inner");
Button save = new Button("Save");
inner.add(save);
outer.add(inner);
outer.setVisible(false);
assertFalse(save.isVisible());
}
Org charts & salary aggregation
31. Model an org chart where an Employee is a leaf and a Department is a composite.
Use BigDecimal for money, never double. Both types implement OrgUnit.
interface OrgUnit {
BigDecimal totalSalary();
int headcount();
}
final class Employee implements OrgUnit {
private final String name;
private final BigDecimal salary;
Employee(String name, BigDecimal salary) { this.name = name; this.salary = salary; }
public BigDecimal totalSalary() { return salary; }
public int headcount() { return 1; }
}
final class Department implements OrgUnit {
private final String name;
private final List members = new ArrayList<>();
Department(String name) { this.name = name; }
void add(OrgUnit unit) { members.add(unit); }
public BigDecimal totalSalary() {
return members.stream().map(OrgUnit::totalSalary).reduce(BigDecimal.ZERO, BigDecimal::add);
}
public int headcount() {
return members.stream().mapToInt(OrgUnit::headcount).sum();
}
}
32. How do you sum total salary for a department including every sub-department?
totalSalary() is recursive: a Department reduces over its members, and any member that is itself a Department contributes its own recursive sum. Using BigDecimal::add as the reducer avoids floating-point rounding drift across a large org.
33. How do you count total headcount under a manager, including reports of reports?
Same shape as salary but summing integers: Employee contributes 1; Department contributes the sum of its members' headcount(). This naturally counts every level without a separate "depth" parameter.
int engineeringSize = engineering.headcount(); // includes every nested team
34. How do you find the highest-paid person anywhere in the organization tree?
Each Employee returns itself as the candidate; each Department compares the best candidate from each of its members and keeps the maximum, bubbling the single best employee up to the root.
public Optional highestPaid() {
if (this instanceof Employee e) return Optional.of(e);
Optional best = Optional.empty();
for (OrgUnit unit : members) {
Optional candidate = unit.highestPaid();
if (candidate.isPresent() && (best.isEmpty()
|| candidate.get().totalSalary().compareTo(best.get().totalSalary()) > 0)) {
best = candidate;
}
}
return best;
}
35. How do you compute average salary per department without double-counting nested teams?
Divide the department's own totalSalary() by its own headcount() — both are already correctly recursive, so the average naturally reflects every employee under that department exactly once, however deep the sub-teams go.
BigDecimal average = department.totalSalary()
.divide(BigDecimal.valueOf(department.headcount()), 2, RoundingMode.HALF_UP);
36. How do you flatten an entire department into a single list of employees?
Add a flatten() operation: Employee returns a singleton list containing itself; Department concatenates the flattened lists of every member. This is effectively a manual visitor that collects rather than aggregates a number.
public List flatten() {
if (this instanceof Employee e) return List.of(e);
List all = new ArrayList<>();
for (OrgUnit unit : members) {
all.addAll(unit.flatten());
}
return all;
}
37. How do you apply an org-wide raise to every employee through the composite structure?
If Employee is mutable, add a raise(BigDecimal percent) operation that Department simply forwards to every member. If Employee is immutable (preferred for salary data), have raise() return a new tree instead of mutating in place.
public OrgUnit raised(BigDecimal percent) {
if (this instanceof Employee e) {
BigDecimal factor = BigDecimal.ONE.add(percent);
return new Employee(e.name(), e.salary().multiply(factor).setScale(2, RoundingMode.HALF_UP));
}
Department copy = new Department(name);
for (OrgUnit unit : members) {
copy.add(unit.raised(percent));
}
return copy;
}
38. How do you find an employee's management chain from leaf up to the CEO?
This is the one operation the recursive-children-only model cannot answer by itself — you need a reference back up the tree. Store a nullable parent field set when a unit is added to a Department, then walk it upward.
public List chainToRoot() {
List chain = new ArrayList<>();
OrgUnit current = this;
while (current != null) {
chain.add(current);
current = current.parent();
}
return chain;
}
add() must also set child.parent = this, and every remove() must clear it — otherwise the chain can point at a stale or wrong ancestor.39. How do you safely move a department under a different VP without introducing a cycle?
Before reattaching, walk the target's own ancestor chain (using the parent references from the previous question) and reject the move if the department being moved appears anywhere in that chain — that would mean a department becomes an ancestor of itself.
void moveUnder(Department newParent) {
OrgUnit walker = newParent;
while (walker != null) {
if (walker == this) {
throw new IllegalArgumentException("Cannot move a department under its own descendant");
}
walker = walker.parent();
}
this.parent().remove(this);
newParent.add(this);
}
40. How do you generate a JSON representation of the org chart recursively?
Add a toJson() operation (or use a library visitor). Employee emits a flat object; Department emits itself plus a JSON array built by recursively serializing each member.
public String toJson() {
if (this instanceof Employee e) {
return "{\"name\":\"" + e.name() + "\",\"salary\":" + e.salary() + "}";
}
String membersJson = members.stream()
.map(OrgUnit::toJson)
.collect(Collectors.joining(",", "[", "]"));
return "{\"department\":\"" + name + "\",\"members\":" + membersJson + "}";
}
Recursive rendering & traversal
51. What is the general recursive algorithm that every Composite operation follows?
Every operation has the same skeleton: a base case at Leaf that does the real, non-recursive work, and a recursive case at Composite that does something local (optional) and then combines the results of calling the same operation on each child. Once you see this shape, writing a new operation is mostly deciding what "combine" means (sum, max, concatenate, union, side effect only).
52. How do you implement pre-order versus post-order traversal, and when does each matter?
Pre-order visits a node before its children — right for printing a tree top-down. Post-order visits children before the node — right for anything that depends on children being fully processed first, like recursive delete or computing a size that must be known before the parent can report its own.
// Pre-order
void preOrder(Consumer visitor) {
visitor.accept(this);
for (FileSystemComponent child : children) child.preOrder(visitor);
}
// Post-order
void postOrder(Consumer visitor) {
for (FileSystemComponent child : children) child.postOrder(visitor);
visitor.accept(this);
}
53. How do you convert a Composite traversal into an iterative one to avoid a StackOverflowError on very deep trees?
Replace the call stack with an explicit Deque used as a stack. Push the root, then repeatedly pop a node, process it, and push its children.
Deque stack = new ArrayDeque<>();
stack.push(root);
long total = 0;
while (!stack.isEmpty()) {
FileSystemComponent node = stack.pop();
if (node instanceof FileLeaf leaf) {
total += leaf.size();
} else if (node instanceof Directory dir) {
stack.addAll(dir.children());
}
}
54. How do you implement a generic Iterator<Component> over an entire Composite tree?
Wrap the same explicit-stack traversal from the previous answer inside an Iterator, so callers can use a plain for loop without knowing the tree is recursive at all.
final class TreeIterator implements Iterator {
private final Deque stack = new ArrayDeque<>();
TreeIterator(FileSystemComponent root) { stack.push(root); }
public boolean hasNext() { return !stack.isEmpty(); }
public FileSystemComponent next() {
FileSystemComponent node = stack.pop();
if (node instanceof Directory dir) {
List kids = dir.children();
for (int i = kids.size() - 1; i >= 0; i--) stack.push(kids.get(i));
}
return node;
}
}
55. How do you combine Composite with Visitor to add a new operation without touching the node classes?
Add one accept(Visitor v) method to Component that simply calls back into the visitor; put every operation's logic in the Visitor implementation instead of spreading it across node classes. A new export format becomes a new Visitor class, not a new method on every node.
interface FsVisitor { void visitFile(FileLeaf f); void visitDirectory(Directory d); }
// on FileLeaf: public void accept(FsVisitor v) { v.visitFile(this); }
// on Directory: public void accept(FsVisitor v) {
// v.visitDirectory(this);
// for (FileSystemComponent c : children) c.accept(v);
// }
class XmlExportVisitor implements FsVisitor {
private final StringBuilder xml = new StringBuilder();
public void visitFile(FileLeaf f) { xml.append(""); }
public void visitDirectory(Directory d) { xml.append(""); }
}
56. How do you compute the depth (height) of a Composite tree?
Leaf has height 0 (or 1, pick a convention and document it). Composite's height is 1 plus the maximum height among its children, or 0 if it has no children — never forget that empty-children case, since Collections.max on an empty stream throws.
public int height() {
if (children.isEmpty()) return 0;
return 1 + children.stream().mapToInt(FileSystemComponent::height).max().orElse(0);
}
57. How do you implement breadth-first (level-order) traversal over a Composite tree?
Use a Queue instead of a stack: enqueue the root, then repeatedly dequeue a node, process it, and enqueue its children — this visits every node level by level rather than diving deep first.
Queue queue = new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty()) {
FileSystemComponent node = queue.poll();
System.out.println(node.name());
if (node instanceof Directory dir) {
queue.addAll(dir.children());
}
}
58. How do you short-circuit a recursive search once a match is found?
Return an Optional (or throw a private sentinel exception for very hot paths) and check for presence after each child call inside the loop, returning immediately instead of continuing to the remaining siblings — this is exactly what findByName did earlier by returning as soon as found.isPresent().
59. How would you parallelize a Composite operation across children for CPU-heavy per-leaf work?
If children are independent, a Composite can fan out with a parallel stream or explicit Fork/Join subtasks, then combine results once every child finishes — safe as long as the per-leaf work has no shared mutable state.
public long size() {
return children.parallelStream()
.mapToLong(FileSystemComponent::size)
.sum();
}
60. What is a common recursion bug in Composite code?
Two recurring ones: forgetting that Leaf is the base case and accidentally giving it a loop that calls itself (infinite recursion with no children to bottom out on), and allowing a parent-child cycle to form so a Composite operation revisits an ancestor and recurses forever. The fix for the first is a code review checklist item; the fix for the second is the cycle-guard techniques in the safety section.
Transparent vs safe design
61. What is the "transparent" Composite design, and what does it cost?
Transparent design puts every operation — including structural ones like add/remove — on the shared Component interface. Client code never needs to know or check whether it holds a Leaf or a Composite. The cost: Leaf must implement methods that make no sense for it, typically by throwing an exception at runtime instead of the compiler catching the misuse.
62. What is the "safe" Composite design, and what does it cost?
Safe design keeps add/remove only on the Composite subtype, off the shared Component interface. A Leaf simply has no such method to call incorrectly — the compiler enforces it. The cost: code that builds or restructures the tree needs an instanceof check or a cast to reach those methods, so it is not fully polymorphic for construction, only for the read/behavioral operations.
63. Show a transparent Component interface with add/remove, and a Leaf that rejects them.
interface FileSystemComponent {
long size();
void add(FileSystemComponent child);
void remove(FileSystemComponent child);
}
final class FileLeaf implements FileSystemComponent {
// ...
public void add(FileSystemComponent child) {
throw new UnsupportedOperationException("A file cannot contain children");
}
public void remove(FileSystemComponent child) {
throw new UnsupportedOperationException("A file cannot contain children");
}
}
64. Show the safe alternative where add/remove exist only on the Composite type.
interface FileSystemComponent {
long size();
}
final class Directory implements FileSystemComponent {
private final List children = new ArrayList<>();
void add(FileSystemComponent child) { children.add(child); }
void remove(FileSystemComponent child) { children.remove(child); }
public long size() { /* ... */ return 0; }
}
// Building the tree requires knowing the concrete type:
Directory docs = new Directory("docs");
docs.add(new FileLeaf("readme.txt", 128)); // fine — docs is a Directory, not a Component
65. Which style is more idiomatic in modern Java, and why?
Modern Java code generally favors the safe design: throwing UnsupportedOperationException for methods that "shouldn't" exist is a code smell the language itself warns against (see List.of()'s immutable lists doing the same thing reluctantly). Safe design also plays well with sealed interfaces and pattern matching, which make the occasional cast feel like a natural switch rather than an awkward downcast.
66. How does Java's pattern matching for switch make the safe design more ergonomic?
A sealed Component hierarchy plus a switch pattern lets the compiler verify every case is handled, so the "cast to Composite" step in safe design reads as an exhaustive, checked branch instead of an unchecked instanceof guess.
sealed interface FileSystemComponent permits FileLeaf, Directory {}
String describe(FileSystemComponent c) {
return switch (c) {
case FileLeaf f -> f.name() + " (" + f.size() + " bytes)";
case Directory d -> d.name() + "/ (" + d.children().size() + " entries)";
};
}
67. Should getParent() live on Component? What does adding it cost?
Putting a parent reference on the shared interface makes upward operations (breadcrumbs, management chains, cycle checks) available uniformly on Leaf and Composite alike. The cost is bookkeeping discipline: every add() must set the child's parent, every remove() must clear it, and every reparent must update both — miss one and traversal upward silently returns a stale or wrong path.
68. Should a Leaf be mutable or immutable, and why does that matter for tree sharing?
Immutable leaves (like the FileLeaf/Employee examples above) are safe to share across multiple parents or to cache, because nothing can change out from under a holder. A mutable Leaf that is accidentally referenced by two Composites can produce surprising updates in both places at once — if sharing a Leaf across parents is intentional, document it; if not, defensively copy on insert.
69. How do you document or test which operations are legal on which node kind under transparent design?
Write explicit unit tests that assert the expected exception, so the contract is enforced by CI rather than relying on Javadoc alone.
@Test
void leafRejectsAddingChildren() {
FileLeaf file = new FileLeaf("a.txt", 10);
assertThrows(UnsupportedOperationException.class,
() -> file.add(new FileLeaf("b.txt", 5)));
}
70. When would you use a marker method like isComposite() instead of instanceof?
Rarely, and mostly as a stepping stone — a boolean marker still requires the caller to branch and cast manually afterward, so it buys little over instanceof pattern matching. It can help in codebases that predate pattern matching or where the type check needs to cross a boundary (like serialized data) where instanceof is not directly available.
Cycle detection & safety
71. Why can Composite trees accidentally become graphs, and why is that dangerous?
Nothing in a plain add(child) method stops a caller from adding a node's own ancestor as its child, since add just appends to a list. Once that happens, the "tree" has a cycle, and any recursive operation (size, print, traversal) that assumes it will eventually reach leaves will recurse forever instead, ending in a StackOverflowError.
72. How do you detect a cycle before calling add(child)?
Before attaching, check whether this (the prospective parent) already appears in the subtree rooted at child — if it does, attaching would close a loop back on itself.
boolean createsCycle(FileSystemComponent candidateChild) {
if (candidateChild == this) return true;
if (candidateChild instanceof Directory dir) {
for (FileSystemComponent c : dir.children()) {
if (createsCycle(c)) return true;
}
}
return false;
}
73. Show a safe addChild() that walks the ancestor chain to reject a cycle before it forms.
If nodes keep parent references, the cheaper check is to walk upward from the new parent and confirm the child being added is not already one of its own ancestors.
void addChild(Directory newParent, FileSystemComponent child) {
OrgUnitLikeCheck: {
FileSystemComponent walker = newParent;
while (walker != null) {
if (walker == child) {
throw new IllegalArgumentException("Would create a cycle: "
+ child.name() + " is already an ancestor of " + newParent.name());
}
walker = walker.parent();
}
}
newParent.children().add(child);
child.setParent(newParent);
}
74. How do you prevent a node from being added as its own direct child?
This is the simplest case of the general cycle check — reject immediately when the candidate child is reference-equal to the parent doing the adding, before running any deeper traversal.
void add(FileSystemComponent child) {
if (child == this) {
throw new IllegalArgumentException("A directory cannot contain itself");
}
children.add(child);
}
75. How do you use a visited-set during traversal to stay safe even if a cycle somehow slips through?
Track visited nodes by identity (not equals, since two distinct nodes might compare equal) using an IdentityHashMap-backed set, and skip any node already seen — this makes traversal cycle-tolerant as a last line of defense on top of, not instead of, prevention at insertion time.
Set visited = Collections.newSetFromMap(new IdentityHashMap<>());
void safeTraverse(FileSystemComponent node) {
if (!visited.add(node)) return; // already seen, skip to avoid infinite loop
System.out.println(node.name());
if (node instanceof Directory dir) {
for (FileSystemComponent child : dir.children()) {
safeTraverse(child);
}
}
}
76. Is it valid for two different Composite parents to hold the same child? Is that a cycle?
Sharing a child between two parents (a DAG, not strictly a tree) is not a cycle by itself and can be a valid, intentional design — for example, a shared "Common" library folder referenced from two projects, or a reusable UI component embedded in two panels. It only becomes dangerous if that shared node's own subtree eventually loops back to include one of its parents. If sharing is intentional, make sure aggregation operations like size() are allowed to double-count, or explicitly deduplicate by identity.
77. How do you make add() thread-safe when multiple threads mutate the same Composite concurrently?
Either back the children collection with a concurrency-safe structure, or guard mutation with a lock; read-mostly trees often do better with a copy-on-write structure than with locking every read.
final class Directory implements FileSystemComponent {
private final List children = new CopyOnWriteArrayList<>();
void add(FileSystemComponent child) { children.add(child); }
// reads iterate a stable snapshot with no external locking needed
}
78. How do you defend against StackOverflowError from pathologically deep composite trees?
Two complementary defenses: cap the maximum depth allowed when building the tree from untrusted input (reject or truncate beyond, say, 500 levels), and prefer the iterative stack-based traversal shown earlier for operations that must run over arbitrary, possibly attacker-controlled depth.
void add(FileSystemComponent child, int currentDepth) {
if (currentDepth > MAX_DEPTH) {
throw new IllegalStateException("Tree exceeds maximum allowed depth");
}
children.add(child);
}
79. How would you write a unit test that specifically asserts a cycle is rejected?
@Test
void movingDepartmentUnderItsOwnChildIsRejected() {
Department engineering = new Department("Engineering");
Department backend = new Department("Backend");
engineering.add(backend);
assertThrows(IllegalArgumentException.class,
() -> engineering.moveUnder(backend));
}
80. What is the risk of allowing null children in a Composite's child list?
A null entry silently breaks every recursive operation the moment it calls a method on it, throwing a NullPointerException deep inside a traversal that gives little context about which insertion caused it. Guard at the door with Objects.requireNonNull(child) inside add(), so the failure happens at the mistake, not three method calls later.
void add(FileSystemComponent child) {
children.add(Objects.requireNonNull(child, "child must not be null"));
}
Composite vs related patterns
81. Composite vs Decorator — what's structurally different even though both "nest" objects?
A Decorator wraps exactly one inner component and typically forms a linear chain (A wraps B wraps C); its purpose is adding responsibility to a single object dynamically. A Composite node holds zero, one, or many children and forms a branching tree; its purpose is representing whole-part structure. Confusing the two usually shows up as a "Composite" that only ever has one child, which is really a Decorator in disguise.
| Composite | Decorator | |
|---|---|---|
| Shape | Tree, 0..N children | Linear chain, exactly 1 wrapped object |
| Intent | Uniform whole-part treatment | Add behavior dynamically |
| Typical operation | Aggregate/recurse over children | Delegate then augment |
82. Composite vs Visitor — how do they complement each other?
Composite defines the tree's shape and how to walk it; Visitor defines what to do at each node without modifying the node classes. Together, adding a new tree-wide operation (export to XML, compute statistics) becomes writing one new Visitor rather than adding a method to every Leaf and Composite class in the hierarchy — see question 55 for the code.
83. Composite vs Iterator — how does Iterator help traverse a Composite without exposing internal structure?
Composite's own recursive methods are one way to traverse, but callers who just want "give me every node" without caring about tree depth benefit from a plain Iterator<Component> (as built in question 54). The client then writes an ordinary for loop, unaware that a stack-based recursive walk is happening underneath.
84. Composite vs plain recursive data classes (like a JSON tree) — when is the formal pattern worth it?
A generic JSON-like tree is fine for pure data interchange with no behavior. Reach for Composite once the tree needs typed operations attached to it — validation rules that differ by node kind, aggregation that must use domain types like BigDecimal, or invariants (a Leaf must never have children) that a generic Map cannot enforce at compile time.
85. Composite vs Flyweight — can Leaf nodes be shared to save memory in huge trees?
Yes, when many leaves are identical and immutable (for example, thousands of "blank cell" leaves in a huge spreadsheet-like tree), apply Flyweight to the Leaf type: a factory returns the same cached immutable instance for identical leaf data instead of allocating a new object per position. Composite still holds references to these shared leaves; only the leaf allocation strategy changes.
86. How do you use a Builder to construct deeply nested Composite trees fluently?
A Builder hides the repetitive new Directory(...); parent.add(child); calls behind a fluent, indentation-like API, which reads much closer to the tree shape it produces.
Directory tree = DirectoryBuilder.dir("root")
.file("README.md", 512)
.dir("src")
.file("App.java", 2048)
.end()
.build();
87. Composite vs a single node class with a "type" enum field — trade-offs of polymorphism vs data-plus-switch?
A single class with a NodeType enum and a switch in every method avoids defining multiple classes, but every new node kind means editing every existing method's switch, and nothing stops a "leaf-typed" instance from having a non-empty children list. Composite's separate classes make illegal states unrepresentable (Leaf literally has no children field) at the cost of one interface plus a couple of small classes.
88. How does the Interpreter pattern relate to Composite?
An Interpreter's abstract syntax tree is, structurally, a Composite tree: terminal expressions are leaves, non-terminal expressions (like AndExpression holding two sub-expressions) are composites that recursively call interpret() on their children. Interpreter is really "Composite plus a specific operation named interpret, applied to a grammar."
89. Composite vs Chain of Responsibility — how do you keep the delegation direction straight?
A Composite operation delegates to every child and combines all their results — it is a fan-out. Chain of Responsibility passes a request to one handler at a time, moving to the next only if the current one declines — it is a fan-through. Mixing them up leads to bugs like a "chain" that (wrongly) asks every handler regardless of whether an earlier one already handled the request.
90. Would you combine Composite with Command to represent an undoable batch of operations organized as a tree?
Yes — a CompositeCommand can hold a list of child commands and implement execute()/undo() by delegating to each child in order (and in reverse order for undo), giving you nested "macro" commands built entirely from the Composite shape applied to the Command interface instead of to domain objects.
interface Command { void execute(); void undo(); }
final class CompositeCommand implements Command {
private final List steps = new ArrayList<>();
void add(Command step) { steps.add(step); }
public void execute() { steps.forEach(Command::execute); }
public void undo() {
for (int i = steps.size() - 1; i >= 0; i--) steps.get(i).undo();
}
}
Post a Comment
Add