Course Content
Object-Oriented Design Interview
14 sections · 29 lessons
File Search: requirements and the file-tree Composite
A file search tool looks like a small utility, and that is exactly why interviewers like it: with five or six classes and no domain complexity, nothing hides a weak model. This lesson covers the prompt, the requirement that decides the score, and the first of two places the Composite pattern appears — the file tree itself.
What makes this problem a favourite
It is small. There are five or six classes and no domain complexity, so nothing hides a weak model. And it has one clear right answer that most candidates miss on the first attempt: the same structural idea — Composite — applies twice, once to the file tree and once to the filters. A candidate who spots the second application is visibly stronger than one who does not, and it takes fifteen seconds to check.
It also punishes feature-counting. A candidate who implements eight filters with an if-else selector scores below one who implements three filters that combine arbitrarily.
The four questions that change the model
1. "Which criteria do I need to support?" Get a list: name (exact, prefix, glob?), extension, size (greater than, less than, between), modification date, permissions, owner. The specific list matters less than the count — more than two means the extensibility requirement is real.
2. "Should criteria combine with AND and OR? Nesting?" This is the decisive question. find . -name "*.log" -size +10M is an implicit AND. If the answer includes OR and nesting — "all .log files, or anything over 100 MB" — you need the filter Composite from The filter design rather than a list of filters applied in sequence.
Ask it explicitly, because if the interviewer says "AND only" you can write ten fewer lines, and if they say "arbitrary combinations" you know what the round is about.
3. "Is the filesystem in memory, or am I calling the operating system?" Assume an in-memory tree behind an interface. The traversal logic is identical, and it makes the design testable without a real disk. Say this: "I'll model the tree as objects behind an interface; a real implementation would wrap the operating system's directory calls."
4. "Case sensitivity, hidden files, and symbolic links?" Small questions that show you have used a filesystem. Case sensitivity should be a filter option, not a global. Symbolic links introduce cycles, which the section's extensions handle.
Assumptions this lesson makes
- An in-memory tree of files and directories behind an interface; a real implementation would read the disk.
- Filters: name (with glob), extension, minimum and maximum size, modified after a date.
- Combinations: AND, OR, NOT, arbitrarily nested.
- Trees up to a few hundred thousand entries — deep enough that recursion depth is worth mentioning.
- Single-threaded search; parallelism appears in the extensions.
Requirements
Functional requirements
1. Walk a directory tree from a given root2. Match an entry against a criterion: name, extension, size, modified date3. Combine criteria with AND, OR, and NOT, nested to any depth4. Return every matching file5. Support a search that stops early (first N matches)Non-functional requirements
One matters more than everything else, and it is not stated in the prompt:
N1. Adding a new criterion must not modify any existing class. This is the Open/Closed Principle (the SOLID reference), and this problem exists to test it. If adding "search by owner" means editing an enum, a switch, and the search runner, the design has failed regardless of how many filters it supports.
N2. Deep trees must not blow the stack. A recursive walk on a tree 10,000 levels deep overflows. Real filesystems are rarely that deep, but a symbolic-link cycle makes any tree infinitely deep, which is why this pairs with the cycle-detection extension.
N3. Directories that cannot match should not be descended. If a filter says "only inside /var/log", walking /home is wasted work. Traversal and filtering, in code covers where to apply this and why it is subtle.
What is cut
CUT (named, not forgotten)- Content search (grep) — mentioned in extensions- Indexing and a persistent index (that is a different problem: locate, not find)- Permissions and ownership filters (same shape as the ones built; no new ideas)- Concurrent traversalNote the third one specifically. Saying "owner and permission filters are the same shape as the size filter — one more class each, no new design" is better than implementing them, because it shows you understand that the design is finished, not the feature list.
The requirement to say out loud
Read the non-functional list back with this framing:
"I want to call out the requirement that isn't in the prompt: adding a new criterion should be a new class, not an edit. I'm going to design for that rather than for the number of filters, because the filter list will grow forever and the code that walks the tree shouldn't."
That sentence, in the first ten minutes, tells the interviewer you know what the problem is testing.
Finding the objects
The candidate nouns
File, directory, filesystem, entry, name, extension, size, modification date, filter, criterion, search, result, path.
The first Composite: the file tree
A file and a directory are different things. A file has bytes and a size; a directory has children and no size of its own. The naive model gives them separate classes with no relationship, and then the traversal code has to keep asking which one it is holding:
1// The version to show first, and then improve2public void search(Object entry, List<File> results) {3 if (entry instanceof File f) {4 if (matches(f)) results.add(f);5 } else if (entry instanceof Directory d) {6 for (Object child : d.getChildren()) search(child, results); // children of what type?7 }8}Directory.getChildren() returns a list of what? It has to hold both files and directories, so it needs a common type — and once you have written that common type, the instanceof checks disappear.
Composite (The seven patterns that actually appear) is exactly this: a common interface, a leaf, and a container that holds the interface type and recurses.
1public abstract class FileSystemEntry {2 protected final String name;3 protected final Instant modifiedAt;4 protected Directory parent;56 public abstract long size(); // file: its bytes; directory: sum of children7 public abstract boolean isDirectory();8 public String path() {9 return parent == null ? name : parent.path() + "/" + name;10 }11}1213public class FileEntry extends FileSystemEntry {14 private final long sizeBytes;15 public long size() { return sizeBytes; }16 public boolean isDirectory() { return false; }17 public String extension() {18 int dot = name.lastIndexOf('.');19 return dot < 0 ? "" : name.substring(dot + 1).toLowerCase();20 }21}2223public class Directory extends FileSystemEntry {24 private final List<FileSystemEntry> children = new ArrayList<>();25 public long size() { return children.stream().mapToLong(FileSystemEntry::size).sum(); }26 public boolean isDirectory() { return true; }27 public void add(FileSystemEntry child) { child.parent = this; children.add(child); }28 public List<FileSystemEntry> getChildren() { return Collections.unmodifiableList(children); }29}Three things this buys, and they are worth naming out loud:
- The recursive walk becomes three lines, because there is no type checking. Traversal and filtering, in code shows it.
size()works on a directory — it is the sum of its children, computed by the same recursion. That is Composite doing real work rather than being ceremony.path()works for free by walking parents upward.
The rest of the model
| Class | Its one-sentence job |
|---|---|
FileSystemEntry | The common type: anything with a name, a size, and a parent |
FileEntry | A leaf with bytes |
Directory | A container of entries, which recurses for size |
Filter | Answers yes or no for one entry — The filter design |
FileSearcher | Walks a tree and returns the entries a filter accepts |
Five classes plus filter implementations. path, name, size, and extension are all fields or derived values, not classes.