Composite Pattern Interview Questions | JiQuest

add

#

Composite Pattern

Java design pattern deep dive

Composite Pattern in Java: 100 interview questions with professional answers.

Learn how to treat individual objects and whole tree structures through one uniform interface, from file systems, org charts, and UI widgets to menus, rule engines, and billing hierarchies, with the recursion, safety, and testing details interviewers actually probe for.

100Questions
4Core roles
2Interface variants
Composite: Rootimplements Component Composite: Folderhas children Leaf: Fileno children Leaf: Fileno children Leaf: Fileno children size()/render() on root recurses through every child uniformly

What makes a good Composite answer?

Interviewers want more than "leaves and composites share an interface." They want to hear where recursion happens, how you keep the design safe versus transparent, and how you avoid the classic tree-shaped bugs.

Uniform interfaceComponent declares the operations every Leaf and Composite must answer to.
Recursive delegationA Composite implements an operation by calling it on each child, then combining results.
Transparent vs safeDecide deliberately whether add/remove live on Component or only on Composite.
Grounded in a real treeName the concrete domain: file system, UI tree, org chart, BOM, or AST.
client callsroot.getTotal() is it a Leaf?return own value is it a Composite?sum each child recurse per childsame call, smaller tree resultbubbles up
ApproachUse whenWatch out for
Transparent Composite (add/remove on Component)Client code should never branch on leaf vs composite, even to build the tree.Leaf must throw or no-op for child-management calls, which strains the Liskov Substitution Principle.
Safe Composite (add/remove only on Composite)Type safety at compile time matters more than perfect uniformity.Client needs an instanceof check or cast before it can build the tree.
Composite plus VisitorNew operations are added often and you don't want to touch every node class each time.Double dispatch adds boilerplate and a new Visitor interface per operation family.
Generic Node<T> treeThe hierarchy is homogeneous with no real per-type behavior difference.Loses domain-specific naming, validation, and the ability to reject invalid children by type.

Topics

File system example Q1Core participants Q2Uniform treatment Q3 Org chart Q4Recursive operations Q5Transparent vs safe Q6 Scene graph Q7Restaurant menu Q8Leaf add() call Q9 Composite vs tree node Q10Product catalog Q11Polymorphism vs instanceof Q12 Permissions ACL Q13Production bug story Q14Composite plus Visitor Q15 Memory overhead Q16Cycle prevention Q17Hierarchy vs flat SQL Q18 equals/hashCode/toString Q19Build system tasks Q20Traversal performance Q21 Refactor switch statement Q22Composite plus Iterator Q23Exposed child list Q24 Caching aggregates Q25Expression tree evaluator Q26Common LSP mistakes Q27 Composite vs Decorator Q28Thread-safe mutation Q29JSON/XML document model Q30 Removing nodes edge cases Q31Open/Closed principle Q32Unit testing composites Q33 Parent reference leaks Q34UI panel layout Q35Migrating tagged unions Q36 Mutable vs immutable Q37Validation rule engine Q38Interface bloat problem Q39 No-op leaf bug Q40Graph-like org structure Q41Composite vs sealed types Q42 Lazy loading children Q43Depth-limited traversal Q44Task scheduler jobs Q45 Abstract class vs interface Q46Bill of materials cost Q47Jackson serialization Q48 Composite plus Command Q49Privilege escalation bug Q50Profiling slow traversal Q51 Composite vs Interpreter Q52Efficient subtree removal Q53Quota checker memoization Q54 DOM querySelector Q55Missing parent reference Q56Fraud rules engine Q57 Nested forum comments Q58Reflection serialization Q59Property-based testing Q60 God Composite anti-pattern Q61Nested test suites Q62Distributed tree nodes Q63 Composite plus Flyweight Q64Find matching leaves Q65Exception handling in children Q66 Network topology devices Q67Structural sharing bugs Q68Visible/enabled flag Q69 Static analysis checks Q70OO tree vs flat array Q71Multi-tenant access tree Q72 Deep clone semantics Q73Refactor to Visitor Q74Path-to-leaf lookup Q75 Recipe ingredient aggregation Q76Mutable editing plus snapshot Q77Composite with Spring DI Q78 Approval workflow escalation Q79Side effects in operations Q80Tree diffing algorithm Q81 Algebraic data types Q82Multi-level cache hierarchy Q83Debugging wrong aggregate Q84 Stack size and recursion Q85Form builder fields Q86Move subtree safely Q87 DB hierarchy vs ltree Q88Fallback strategy chain Q89Metrics and observability Q90 Sync vs async aggregation Q91Idempotent add semantics Q92Role-based menu filtering Q93 Over-applied Composite Q94Nested tax calculation Q95Sorting composite children Q96 Constant folding AST Q97Optional vs exceptions Q98Multi-format report rendering Q99 Paginated tree API Q100

Composite pattern interview questions and answers

Each answer gives the design direction, the trade-off worth stating out loud, and the production concern that makes the answer sound senior rather than textbook.

1. Explain the intent of the Composite design pattern and walk through a Java example modeling a file system where directories can contain both files and other directories.

The Composite pattern's intent is to let clients treat a single object and a group of objects the same way by having both implement one common interface. A file system is the canonical example: a File is a leaf with no children, while a Directory is a composite that holds other FileSystemEntry instances, including nested directories.

public interface FileSystemEntry {
    String getName();
    long getSize();
}

public final class FileEntry implements FileSystemEntry {
    private final String name;
    private final long size;

    public FileEntry(String name, long size) {
        this.name = name;
        this.size = size;
    }

    public String getName() { return name; }
    public long getSize() { return size; }
}

public final class DirectoryEntry implements FileSystemEntry {
    private final String name;
    private final List<FileSystemEntry> children = new ArrayList<>();

    public DirectoryEntry(String name) { this.name = name; }

    public void add(FileSystemEntry entry) { children.add(entry); }

    public String getName() { return name; }

    public long getSize() {
        long total = 0;
        for (FileSystemEntry child : children) {
            total += child.getSize();
        }
        return total;
    }
}

Calling getSize() on the top-level directory recurses through every nested directory automatically; the client never needs to know how deep the tree goes.

2. What are the core participants in the Composite pattern (Component, Leaf, Composite, Client) and what responsibilities does each one hold in a UML class diagram translated to Java interfaces?

Component is the shared interface or abstract class declaring the operations common to both simple and compound objects. Leaf implements Component directly with no children and provides the base-case behavior. Composite also implements Component but additionally stores a collection of child Component references and implements each operation by delegating to those children. Client code depends only on the Component type, never on Leaf or Composite concretely.

Component interfaceLeaf base caseComposite delegationClient depends on abstraction

3. How does the Composite pattern let client code treat individual objects and compositions of objects uniformly, and what specific Java interface design decisions make that uniformity possible?

Uniform treatment comes from the client only ever calling methods declared on Component, so it never needs an if (obj instanceof Leaf) check to decide how to proceed. The key design decision is choosing which operations belong on the shared interface: put only the operations that make sense for both leaves and composites (like render() or getTotalPrice()) at that level, and keep tree-structure-only operations like add()/remove() either absent from Leaf or implemented to fail predictably.

public interface Shape {
    void draw(Graphics2D g);
}
// A client rendering a whole scene never checks types:
for (Shape shape : scene.getShapes()) {
    shape.draw(graphics);
}

4. Walk through implementing an organizational chart in Java using Composite, where an Employee can be either an individual contributor or a Manager who has direct reports.

Model a common Employee interface exposing getTotalReports() and getTotalSalary(). An individual contributor is a leaf returning zero reports and their own salary. A Manager is a composite holding direct reports and summing recursively, so the CEO's total headcount naturally includes every level of the org.

public interface Employee {
    String getName();
    double getSalary();
    int getTotalReports();
}

public final class IndividualContributor implements Employee {
    private final String name;
    private final double salary;

    public IndividualContributor(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() { return name; }
    public double getSalary() { return salary; }
    public int getTotalReports() { return 0; }
}

public final class Manager implements Employee {
    private final String name;
    private final double salary;
    private final List<Employee> directReports = new ArrayList<>();

    public Manager(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public void addReport(Employee employee) { directReports.add(employee); }

    public String getName() { return name; }
    public double getSalary() { return salary; }

    public int getTotalReports() {
        int count = directReports.size();
        for (Employee report : directReports) {
            count += report.getTotalReports();
        }
        return count;
    }
}

5. Describe how the Composite pattern applies recursion internally when operations like calculating total price or rendering a UI tree are invoked on the root component.

Every Composite's implementation of an operation is written in terms of the same operation on its children, not in terms of any lower-level primitive. When the client calls the operation once on the root, each composite node forwards the call downward, leaves return their base value, and the results are combined and passed back up the call stack. The recursion depth equals the tree's height, and the client code stays a single method call regardless of how many nodes exist.

public double getTotalPrice() {
    double total = 0;
    for (Component child : children) {
        total += child.getTotalPrice(); // recursive call, same operation
    }
    return total;
}

6. What is the difference between the 'transparent' and 'safe' variants of the Composite pattern, and what are the concrete Java trade-offs of putting child-management methods (add/remove/getChild) on the base Component interface versus only on Composite?

In the transparent variant, add(), remove(), and getChild() live on Component itself, so client code can call them on any node without casting; the cost is that Leaf must provide some implementation, typically throwing. In the safe variant, those methods exist only on Composite, so a Leaf simply has no such methods and the compiler prevents misuse, at the cost of the client needing an instanceof check or cast before building the tree.

Practical guidance Favor the safe variant when correctness at compile time matters more than uniform tree-building code; favor transparent when the client genuinely builds trees generically and cannot know node types in advance.

7. Show how you would implement a graphical scene graph in Java using Composite, where Shape is the component and Group is a composite that can contain other Shapes or Groups.

A scene graph is one of the most natural Composite use cases because rendering, hit-testing, and bounding-box calculation all need to work the same way whether you are looking at a single shape or an arbitrarily nested group of shapes.

public interface Shape {
    void draw(Graphics2D g);
    Rectangle2D getBounds();
}

public final class Circle implements Shape {
    private final double x, y, radius;
    public Circle(double x, double y, double radius) { this.x = x; this.y = y; this.radius = radius; }
    public void draw(Graphics2D g) { g.draw(new Ellipse2D.Double(x - radius, y - radius, radius * 2, radius * 2)); }
    public Rectangle2D getBounds() { return new Rectangle2D.Double(x - radius, y - radius, radius * 2, radius * 2); }
}

public final class Group implements Shape {
    private final List<Shape> shapes = new ArrayList<>();

    public void add(Shape shape) { shapes.add(shape); }

    public void draw(Graphics2D g) {
        for (Shape shape : shapes) shape.draw(g);
    }

    public Rectangle2D getBounds() {
        Rectangle2D bounds = null;
        for (Shape shape : shapes) {
            bounds = (bounds == null) ? shape.getBounds() : bounds.createUnion(shape.getBounds());
        }
        return bounds == null ? new Rectangle2D.Double() : bounds;
    }
}

8. How would you use the Composite pattern to model a menu system in a restaurant application where a MenuItem could be a single dish or a combo/meal composed of other MenuItems?

Define a MenuItem interface with getPrice() and getCalories(). A Dish is a leaf with fixed values. A Combo is a composite that may apply a discount on top of the sum of its parts, which is a good illustration that a composite's operation does not have to be a pure sum — it can add its own logic around the aggregated result.

public final class Combo implements MenuItem {
    private final String name;
    private final List<MenuItem> items = new ArrayList<>();
    private final double discountPercent;

    public Combo(String name, double discountPercent) {
        this.name = name;
        this.discountPercent = discountPercent;
    }

    public void addItem(MenuItem item) { items.add(item); }

    public double getPrice() {
        double subtotal = items.stream().mapToDouble(MenuItem::getPrice).sum();
        return subtotal * (1 - discountPercent / 100.0);
    }

    public int getCalories() {
        return items.stream().mapToInt(MenuItem::getCalories).sum();
    }
}

9. What happens in a transparent Composite implementation when a client calls add() on a Leaf node, and how should this be handled in Java — throw UnsupportedOperationException, silently no-op, or something else?

Throwing UnsupportedOperationException is the safer choice because a silent no-op hides a genuine programming error: the caller believed it was building a tree correctly and the failure to add a child could cause missing data downstream with no visible symptom. A no-op only makes sense in a UI context where "this node cannot have children" is expected and non-exceptional, and even then it should be logged.

public class Leaf implements Component {
    public void add(Component child) {
        throw new UnsupportedOperationException("Leaf nodes cannot have children");
    }
}
Trade-off This is exactly where the transparent variant strains the Liskov Substitution Principle: a Leaf is-a Component but cannot honor every Component contract.

10. Explain how the Composite pattern differs from simply using a tree data structure with a generic Node<T> class, and when the extra interface abstraction actually earns its complexity in a Java codebase.

A generic Node<T> stores a payload and children uniformly, but every operation ends up switching on the payload's runtime type. Composite instead gives each node type — leaf and composite — its own class with its own behavior, so the type system enforces what each kind of node can do. The abstraction earns its cost when different node types genuinely behave differently (a file computes its own size, a directory sums children) rather than merely differing in a data field.

AspectGeneric Node<T>Composite pattern
Type safetyRuntime casts and type tagsCompile-time polymorphism
New node typeAdd a case to every switchAdd one class implementing Component
Domain namingGeneric, weak self-documentationDirectory, File, Manager — reads as the domain

11. Design a Composite-based representation of an e-commerce product catalog where a Category can contain Products and nested Subcategories, and implement a method to compute the total inventory value.

A CatalogNode interface exposes getInventoryValue(). Product is a leaf computing price * quantityOnHand. Category is a composite holding both products and nested categories in one list, summing over all of them regardless of type.

public interface CatalogNode {
    double getInventoryValue();
}

public final class Product implements CatalogNode {
    private final double price;
    private final int quantityOnHand;

    public Product(double price, int quantityOnHand) {
        this.price = price;
        this.quantityOnHand = quantityOnHand;
    }

    public double getInventoryValue() { return price * quantityOnHand; }
}

public final class Category implements CatalogNode {
    private final String name;
    private final List<CatalogNode> children = new ArrayList<>();

    public Category(String name) { this.name = name; }

    public void add(CatalogNode node) { children.add(node); }

    public double getInventoryValue() {
        return children.stream().mapToDouble(CatalogNode::getInventoryValue).sum();
    }
}

12. What role does polymorphism play in the Composite pattern's ability to avoid explicit type-checking (instanceof) when traversing a tree of mixed leaf and composite nodes?

Because every node overrides the same virtual method with behavior appropriate to its own type, the JVM's dynamic dispatch picks the correct implementation at each call site without the calling code needing to know which one it is. This is what removes the need for a chain of instanceof checks or a type-tag switch: the decision of "what does this node do" is made once, at the class level, instead of repeatedly at every call site.

Dynamic dispatchSingle Responsibility per typeNo type-tag switch

13. How would you implement the Composite pattern for a permissions/ACL system where a Permission could be an individual grant or a PermissionGroup composed of other permissions, including conflict resolution?

Model Permission with an isAllowed(String action) method. A single Grant leaf answers directly for its own action. A PermissionGroup composite must define conflict-resolution semantics explicitly — commonly "deny wins," meaning if any child denies the action, the whole group denies it even if other children allow it.

public interface Permission {
    Decision check(String action);
}

public enum Decision { ALLOW, DENY, NOT_APPLICABLE }

public final class PermissionGroup implements Permission {
    private final List<Permission> members = new ArrayList<>();

    public void add(Permission permission) { members.add(permission); }

    public Decision check(String action) {
        Decision result = Decision.NOT_APPLICABLE;
        for (Permission member : members) {
            Decision decision = member.check(action);
            if (decision == Decision.DENY) return Decision.DENY; // deny wins, short-circuit
            if (decision == Decision.ALLOW) result = Decision.ALLOW;
        }
        return result;
    }
}

14. Describe a real production incident where treating a deeply nested Composite tree uniformly (leaf vs composite) caused a subtle bug, and how you would guard against it with defensive coding or validation.

A common incident: a billing composite summed line items correctly for years, until someone added a composite node representing a "free sample" bundle whose leaf items all had zero price but a non-zero shipping weight field that a newer getShippingWeight() method had never been added to the original Leaf class, so it silently defaulted to zero and under-billed shipping across thousands of orders. The fix is to make every new cross-cutting operation part of the shared interface from day one, and to add an integration test that walks a real production-shaped tree and asserts totals against a hand-computed value.

Guardrail Add a "shape" test: build a tree with at least three levels of nesting and mixed leaf types, and assert every aggregate method against manually computed expected values in CI.

15. How can the Composite pattern be combined with the Visitor pattern to add new operations (e.g., XML export, pretty-printing) to a component tree without modifying the Component, Leaf, and Composite classes themselves?

Each node gains one stable accept(Visitor visitor) method that calls back into the visitor with itself as the argument (double dispatch). New behavior, like an XML exporter or pretty-printer, is added by writing a new Visitor implementation rather than touching every node class. This inverts the usual Composite trade-off: adding a new operation becomes easy, while adding a new node type becomes the expensive change because every Visitor must be updated.

public interface Visitor {
    void visitLeaf(Leaf leaf);
    void visitComposite(Composite composite);
}

public interface Component {
    void accept(Visitor visitor);
}

public final class Leaf implements Component {
    public void accept(Visitor visitor) { visitor.visitLeaf(this); }
}

public final class Composite implements Component {
    private final List<Component> children = new ArrayList<>();
    public void accept(Visitor visitor) {
        visitor.visitComposite(this);
        for (Component child : children) child.accept(visitor);
    }
}

16. What are the memory and object-count implications of using Composite for a very large tree (e.g., a DOM with millions of nodes), and what strategies mitigate excessive object overhead in Java?

Every node in a Java Composite tree carries object header overhead (typically 12-16 bytes), plus a reference for each field, plus the backing ArrayList a composite uses for children — for a DOM with millions of nodes this adds up to real megabytes beyond the payload data itself. Mitigations include using primitive-friendly or more compact collections instead of ArrayList<Component> per node, sharing immutable leaf instances via Flyweight, lazily materializing subtrees, and preferring arrays over lists for children counts that rarely change after construction.

Object header overheadFlyweight sharingLazy materialization

17. Explain how you would detect and prevent cycles when building a Composite tree at runtime, given that a naive add(Component child) implementation could allow a composite to contain itself, causing infinite recursion.

Before accepting a new child, walk from the proposed child upward (or downward, checking if the parent already appears among the child's own descendants) and reject the operation if the parent is found anywhere in that reachable set — this is the same idea as detecting a cycle before adding an edge in a directed graph.

public void add(Component child) {
    if (child == this || containsDescendant(child, this)) {
        throw new IllegalArgumentException("Adding this child would create a cycle");
    }
    children.add(child);
}

private boolean containsDescendant(Component node, Component target) {
    if (!(node instanceof Composite composite)) return false;
    for (Component c : composite.getChildren()) {
        if (c == target || containsDescendant(c, target)) return true;
    }
    return false;
}

18. Compare implementing a company's expense-reporting hierarchy using the Composite pattern versus using a flat list of expenses with a parentId foreign key and computing sums via SQL — what are the trade-offs in each approach?

The Composite (in-memory object tree) approach gives you type-safe traversal, easy addition of new business rules per node type, and simple unit testing without a database, but it requires loading the whole subtree into memory and re-implementing aggregation logic that a database already does well. The flat table with parentId plus a recursive SQL CTE pushes aggregation to the database, scales to millions of rows, and supports ad hoc queries, but loses compile-time type safety and pushes business rules into SQL or application code that has to re-fetch structure on every change.

WITH RECURSIVE expense_tree AS (
    SELECT id, parent_id, amount FROM expenses WHERE id = :rootId
    UNION ALL
    SELECT e.id, e.parent_id, e.amount
    FROM expenses e
    JOIN expense_tree t ON e.parent_id = t.id
)
SELECT SUM(amount) FROM expense_tree;

19. How do you implement equals(), hashCode(), and toString() correctly on Composite and Leaf classes when the tree can be arbitrarily deep, avoiding stack overflows from naive recursive implementations?

A naive recursive equals()/hashCode() that walks every descendant will blow the stack on a sufficiently deep or wide tree, and structural equality on large trees is rarely what you actually want anyway. Prefer identity-based equals()/hashCode() (the default Object behavior, or based on a stable id field) for tree nodes, and implement toString() with a bounded, iterative summary rather than a full recursive dump.

@Override
public String toString() {
    return getClass().getSimpleName() + "{name=" + name + ", childCount=" + children.size() + "}";
}
Avoid Do not implement hashCode() by recursively hashing every child; it is slow, and equality by deep structure usually is not the semantic the domain wants for a mutable tree node.

20. Design a Composite pattern implementation for a build system (like Maven or Gradle) where a Task could be a single build step or a composite Goal made up of other tasks, supporting parallel execution of independent children.

A Task interface exposes execute(ExecutorService pool) returning a CompletableFuture<Void>. A leaf BuildStep runs its own work directly. A composite Goal submits each independent child concurrently and combines their futures, only completing once every child has finished, which mirrors how real build tools parallelize independent modules.

public interface Task {
    CompletableFuture<Void> execute(ExecutorService pool);
}

public final class Goal implements Task {
    private final List<Task> subtasks = new ArrayList<>();

    public void add(Task task) { subtasks.add(task); }

    public CompletableFuture<Void> execute(ExecutorService pool) {
        List<CompletableFuture<Void>> futures = subtasks.stream()
            .map(task -> task.execute(pool))
            .toList();
        return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
    }
}

21. What is the performance cost of traversing a Composite tree recursively for an operation like sum() or count(), and how would you benchmark and optimize it for a tree with deep nesting (say, 10,000 levels)?

Recursive traversal costs one stack frame per level, so a 10,000-level-deep tree risks a StackOverflowError long before it becomes a performance problem in the CPU sense — the JVM's default thread stack (roughly 512KB-1MB) typically cannot sustain tens of thousands of nested frames with non-trivial locals. Benchmark with JMH under realistic tree shapes, and if depth is genuinely unbounded, convert the traversal to an explicit iterative stack-based algorithm using java.util.ArrayDeque instead of the call stack.

public long countIterative(Component root) {
    long count = 0;
    Deque<Component> stack = new ArrayDeque<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        Component node = stack.pop();
        count++;
        if (node instanceof Composite composite) {
            stack.addAll(composite.getChildren());
        }
    }
    return count;
}

22. How would you refactor a bloated switch-statement-based rendering method (checking node.getType()) into a proper Composite pattern implementation, and what maintainability benefits does that refactor provide?

Start by identifying each case in the switch as a candidate class implementing a shared Component interface, then move the body of each case into that class's own method implementation. Once every case has become a class, delete the switch entirely and replace call sites with a single polymorphic call. The main benefit is that adding a new node type no longer requires hunting down every switch statement across the codebase — you add one new class instead, and the compiler will flag any interface method you forgot to implement.

Replace conditional with polymorphismOpen/Closed PrincipleSingle source of truth per type

23. Explain how Composite interacts with the Iterator pattern when you need to traverse a tree of components in different orders (pre-order, post-order, breadth-first) without exposing internal structure.

Rather than have client code manually recurse (which forces it to know about the composite's internal children collection), expose an iterator() method on Component that returns an Iterator<Component> pre-configured for the desired traversal order. The client then loops with a standard for-each, completely decoupled from whether the traversal is pre-order, post-order, or breadth-first internally.

public Iterator<Component> preOrderIterator() {
    List<Component> result = new ArrayList<>();
    collectPreOrder(this, result);
    return result.iterator();
}

private void collectPreOrder(Component node, List<Component> out) {
    out.add(node);
    if (node instanceof Composite composite) {
        for (Component child : composite.getChildren()) {
            collectPreOrder(child, out);
        }
    }
}

24. What are the risks of exposing a composite's internal child list directly via getChildren() returning a mutable List, and how would you fix this using an unmodifiable view or defensive copy in Java?

If getChildren() returns the live backing ArrayList, any caller can add, remove, or reorder children behind the composite's back, bypassing cycle checks, cache invalidation, or parent-reference bookkeeping the composite's own add()/remove() methods were supposed to enforce. Wrap the return value in Collections.unmodifiableList() or return an immutable copy so mutation is only possible through the composite's own controlled methods.

private final List<Component> children = new ArrayList<>();

public List<Component> getChildren() {
    return Collections.unmodifiableList(children);
}
Common bug Returning the raw field lets a caller call getChildren().clear() and silently wipe an entire subtree with no validation or event firing.

25. Describe how to implement caching of expensive aggregate computations (like total size or total cost) in a Composite tree, including cache invalidation when a child is added, removed, or mutated.

Store a nullable cached value and a dirty flag on each composite. On mutation (add, remove, or a leaf's value changing), invalidate not just the local cache but propagate the invalidation up to every ancestor, since their cached aggregates depend on this subtree too. This requires either a parent reference or an explicit invalidation callback passed down when the tree is built.

public final class CachingDirectory implements FileSystemEntry {
    private Long cachedSize;
    private CachingDirectory parent;
    private final List<FileSystemEntry> children = new ArrayList<>();

    public long getSize() {
        if (cachedSize == null) {
            cachedSize = children.stream().mapToLong(FileSystemEntry::getSize).sum();
        }
        return cachedSize;
    }

    public void invalidate() {
        cachedSize = null;
        if (parent != null) parent.invalidate();
    }
}
No comments
Leave a Comment