Definition
- FileSystemNode – abstract base with name, permissions, and operations like
size()anddelete(). - File – a leaf node that stores content and returns its own byte size.
- Directory – a composite that holds child nodes (files and sub-directories) and computes its size by summing children recursively.
- Path resolution – walks the tree segment by segment from the root.
- Permissions – read, write, execute flags checked before every operation.
- Operations – move (re-parent), copy (deep-clone), delete (recursive for directories).
Think of National Bank of India's document archive. A folder can hold documents and more folders, which can hold yet more folders, endlessly nested. Rajesh's account folder contains statements and a sub-folder of tax documents. Whether you point at a single statement or a whole folder, you can ask its size or move it, because folders and documents share the same basic behaviour. That uniform treatment is the Composite pattern.
Understanding the Concepts
What is the core design?
The Composite pattern is the heart of the design. An abstract FileSystemNode declares operations that both files and directories support. A File is a leaf. A Directory is a composite that holds children.
package com.bank.fs.model;
public abstract class FileSystemNode {
private final String name;
private Permission permission;
private Directory parent;
protected FileSystemNode(String name, Permission permission) {
this.name = name;
this.permission = permission;
}
public abstract long size();
public abstract void delete();
public String getName() { return name; }
public Permission getPermission() { return permission; }
public Directory getParent() { return parent; }
public void setParent(Directory parent) { this.parent = parent; }
public String getPath() {
if (parent == null) return "/" + name;
return parent.getPath() + "/" + name;
}
}
package com.bank.fs.model;
public class Permission {
private final boolean read;
private final boolean write;
private final boolean execute;
public Permission(boolean read, boolean write, boolean execute) {
this.read = read;
this.write = write;
this.execute = execute;
}
public boolean canRead() { return read; }
public boolean canWrite() { return write; }
public boolean canExecute() { return execute; }
}
How does the File leaf work?
A File is a leaf that stores content and returns its own byte size.
package com.bank.fs.model;
public class File extends FileSystemNode {
private byte[] content;
public File(String name, Permission permission, byte[] content) {
super(name, permission);
this.content = content;
}
@Override
public long size() {
return content.length;
}
@Override
public void delete() {
System.out.println("Deleted file: " + getPath());
}
public byte[] getContent() { return content; }
}
How does the Directory composite work?
A Directory holds child nodes and computes its size by summing its children recursively.
package com.bank.fs.model;
import java.util.*;
public class Directory extends FileSystemNode {
private final Map<String, FileSystemNode> children;
public Directory(String name, Permission permission) {
super(name, permission);
this.children = new LinkedHashMap<>();
}
public void addChild(FileSystemNode node) {
if (children.containsKey(node.getName())) {
throw new IllegalStateException(
"Node " + node.getName() + " already exists");
}
node.setParent(this);
children.put(node.getName(), node);
}
public void removeChild(String name) {
children.remove(name);
}
public FileSystemNode getChild(String name) {
return children.get(name);
}
@Override
public long size() {
return children.values().stream()
.mapToLong(FileSystemNode::size)
.sum();
}
@Override
public void delete() {
for (FileSystemNode child : new ArrayList<>(children.values())) {
child.delete();
}
children.clear();
System.out.println("Deleted directory: " + getPath());
}
public Collection<FileSystemNode> getChildren() {
return children.values();
}
}
long total = directory.size(); // recurses over all children
How does path resolution work?
Path resolution walks the tree segment by segment from the root.
package com.bank.fs.service;
import com.bank.fs.model.*;
public class FileSystemService {
private final Directory root;
public FileSystemService(Directory root) {
this.root = root;
}
public FileSystemNode resolve(String path) {
String[] segments = path.split("/");
FileSystemNode current = root;
for (String segment : segments) {
if (segment.isEmpty()) continue;
if (!(current instanceof Directory dir)) {
throw new IllegalStateException(
segment + " is not a directory");
}
current = dir.getChild(segment);
if (current == null) {
throw new IllegalStateException(
"Not found: " + segment);
}
}
return current;
}
}
How does search work?
Search is a recursive traversal matching a predicate.
public List<FileSystemNode> search(Directory dir, String namePattern) {
List<FileSystemNode> results = new ArrayList<>();
for (FileSystemNode child : dir.getChildren()) {
if (child.getName().contains(namePattern)) {
results.add(child);
}
if (child instanceof Directory subDir) {
results.addAll(search(subDir, namePattern));
}
}
return results;
}
How do move, copy, and delete work?
Move re-parents a node by removing it from its current directory and adding it to the target. Copy deep-clones a subtree. Delete on a directory is recursive.
public void move(FileSystemNode node, Directory target) {
if (isDescendant(node, target)) {
throw new IllegalStateException(
"Cannot move a directory into its own descendant");
}
node.getParent().removeChild(node.getName());
target.addChild(node);
System.out.println("Moved " + node.getName()
+ " to " + target.getPath());
}
private boolean isDescendant(FileSystemNode ancestor,
FileSystemNode node) {
FileSystemNode current = node;
while (current != null) {
if (current == ancestor) return true;
current = current.getParent();
}
return false;
}
What are the edge cases?
Edge cases are where a careful design shows:
- Prevent cycles. A directory cannot contain itself. The
moveoperation checks that the target is not a descendant of the node being moved. - Recursive deletes must be explicit. Deleting a non-empty directory recurses through all children.
- Duplicate names.
addChildrejects a child with a name that already exists in the directory. - Permissions. Every operation checks permissions before proceeding.
What should a senior engineer know?
- The Composite pattern gives uniform treatment: whether a client points at a file or a folder, it calls the same
size()ordelete()method. - In production, file systems use B-trees or hash indexes for directory lookups, not linear maps.
- Copy of large subtrees should be lazy (copy-on-write) for performance.
- Real file systems handle hard links and symbolic links, which break the pure tree assumption.
What are common interview follow-up questions?
- How would you add symbolic links?
- How do you handle permissions inheritance from parent to child?
- How would you implement a recycle bin (soft delete)?
- What is the time complexity of computing a directory's size?
- How would you optimise search for a file system with millions of files?
Visual Overview
Composite Pattern: File System Tree
────────────────────────────────────
root (Directory)
├── rajesh (Directory)
│ ├── statements (Directory)
│ │ ├── jan-2025.pdf (File, 45KB)
│ │ └── feb-2025.pdf (File, 38KB)
│ └── tax (Directory)
│ └── form-16.pdf (File, 120KB)
└── priya (Directory)
└── loan-agreement.pdf (File, 95KB)
root.size() = 45 + 38 + 120 + 95 = 298 KB
FileSystemNode (abstract)
│
├── File (leaf)
│ size() → own bytes
│
└── Directory (composite)
size() → sum of children.size()
addChild(), removeChild()
Key Takeaway
When you design a file system, reach for the Composite pattern immediately, because a file system is fundamentally a recursive tree where a directory contains files and other directories without limit. Define an abstract file-system node with a name, permissions, and operations like size and delete. A File is a leaf that knows its own byte size, and a Directory is a composite that holds children and computes its size by summing its children recursively.
The power of this design is uniform treatment. Whether a client points at a single file or an entire folder, it calls the same size() or delete() operation, and the recursion handles arbitrary nesting automatically. Asking a top-level folder for its size naturally descends through every sub-folder and file beneath it.
The operations follow from the tree structure. Path resolution walks the tree segment by segment from the root. Search is a recursive traversal matching a predicate. Move re-parents a node by removing it from one directory and adding it to another. Copy deep-clones a subtree. Permissions are checked before every operation.
The edge cases are where a careful design shows. Prevent cycles so a directory can never contain itself. Make deletes on non-empty directories recursive and deliberate. Reject moving a directory into its own descendant because that would corrupt the tree. At National Bank of India, the document archive models customer folders nesting statements and tax sub-folders, and the Composite pattern lets the team report folder sizes and move documents cleanly regardless of depth.
Remember one sentence: a file system is a Composite-pattern tree where files are leaves, directories are composites, and every operation works uniformly regardless of nesting depth.
---