9. File System Intermediate → Advanced

A. Realistic Interview Prompt

"Design an in-memory file system supporting files and directories. Support creating, deleting, moving, reading, and writing files; listing directory contents; searching for files by name or extension; and computing disk usage for any directory."

B. Difficulty

Intermediate → Advanced. The tree structure itself is not hard — the difficulty is entirely in what a naive candidate bolts onto the base node type (every possible read-only operation, permission logic, locking) versus what a mature candidate keeps separate via Composite + Visitor + a disciplined locking protocol.

C. What This Problem Tests

D. Requirements

Functional

  1. Create/delete files and directories (deleting a non-empty directory requires an explicit recursive flag)
  2. Read/write file contents
  3. Move/rename a file or directory, including across different parent directories
  4. List a directory's immediate contents
  5. Search for files by name, extension, or size under a given subtree
  6. Compute total disk usage of any directory (recursive size)

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1In-memory only, or must it eventually persist to real disk?Determines whether a pluggable StorageBackend boundary is needed now or can be deferredScope of X (Persistence)
2Is per-user permission enforcement required, or just structural correctness?Determines whether a Proxy access-control layer is in scopePresence of SecureFileSystemProxy
3Will this be accessed by multiple threads concurrently?Determines whether locking is a core requirement or a stretch extensionScope of W (Concurrency)
4Are symbolic or hard links in scope?Links reintroduce exactly the cycle risk that plain moves must already prevent, at a much larger scaleExcluded by assumption (see F)
5Does search need wildcard/regex matching, or is exact/substring matching enough?Affects whether search is a linear Visitor pass or needs a name indexScope of SearchVisitor vs. an indexed alternative

F. Assumptions

G. Actors

ActorResponsibility
ClientCalls FileSystem operations (create, read, write, move, search)
FileSystemFacade; resolves paths, coordinates locking, delegates to nodes

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
FileSystemNode (abstract)Common identity for anything in the treename, parentaccept(visitor)Any read-only aggregate operation — that's a Visitor's job (see I)
File (leaf)Holds content, reports its own sizecontent, sizeread(), write(data)Anything about children — it has none
Directory (composite)Holds children uniformlyMap<String, FileSystemNode> childrenaddChild(), removeChild(), getChild(name)Recursive size/search — delegated to whichever Visitor is passed in
Path (value object)Normalized location, independent of the live treeList<String> segmentsresolve(), parent()Anything about whether the path actually exists
NodeVisitor (interface)One read-only operation applied across the tree—visitFile(f), visitDirectory(d)Mutation — visitors in this design are read-only by convention
DirectoryIteratorTraverses a directory's children—hasNext(), next()Deciding fail-fast vs. snapshot semantics — that's a documented policy choice, not hidden behavior (see AB)
FileSystem (facade)Path resolution, lock acquisition/ordering, top-level APIroot DirectorycreateFile(), createDirectory(), delete(), move(), read(), write(), list(), search(), diskUsage()Node-level storage details

I. Class Relationships

J. Class Diagram

children

*

root

accept

FileSystemNode

name

parent

+accept(visitor)

File

content

size

+read()

+write(data)

Directory

children

+addChild()

+removeChild()

+getChild(name)

«interface»

NodeVisitor

+visitFile(f)

+visitDirectory(d)

FileSystem

root

+createFile()

+move()

+search()

+diskUsage()

DiskUsageVisitor

SearchVisitor

View Mermaid source
classDiagram
  class FileSystemNode {
    name
    parent
    +accept(visitor)
  }
  class File {
    content
    size
    +read()
    +write(data)
  }
  class Directory {
    children
    +addChild()
    +removeChild()
    +getChild(name)
  }
  class NodeVisitor {
    <<interface>>
    +visitFile(f)
    +visitDirectory(d)
  }
  class FileSystem {
    root
    +createFile()
    +move()
    +search()
    +diskUsage()
  }
  FileSystemNode <|-- File
  FileSystemNode <|-- Directory
  Directory --> "*" FileSystemNode : children
  FileSystem --> Directory : root
  NodeVisitor <|.. DiskUsageVisitor
  NodeVisitor <|.. SearchVisitor
  FileSystemNode --> NodeVisitor : accept

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
FileSystemNodeIdentity, dispatch to visitorsAny aggregate computation
FileOwn content and sizeAnything about siblings/children
DirectoryOwn children mapRecursive aggregation (Visitor's job)
NodeVisitor implementationsOne specific read-only computation over the treeMutating the tree
FileSystemPath resolution, lock acquisition order, top-level orchestrationNode-internal storage

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation
SRPFile owns content; Directory owns children; visitors own aggregate logicOne Node class with an isDirectory flag, content field, and a children list, all always present, most always unused
OCPNew read-only operation = new NodeVisitor implementation, zero changes to node classesAdding countImages() as yet another method on FileSystemNode, forcing every existing node subclass to be touched again
LSPFile and Directory both honestly implement accept(visitor); a File visited by visitFile() never needs to fake directory behaviorCalling listChildren() on a File node and having it silently return an empty list instead of the operation being structurally unavailable
ISPNodeVisitor stays a two-method interface; new capabilities are new visitor classes, not new methods forced onto FileSystemNodeA fat FileSystemNode interface with size(), search(), countFiles(), checksum() all bolted on directly
DIPFileSystem depends on FileSystemNode/NodeVisitor abstractions, not concrete File/Directory branchesFileSystem.diskUsage() doing its own instanceof Directory recursion inline instead of delegating to a visitor

M. Design Patterns

PatternProblem It SolvesWhere It AppearsWhen NOT to Use
CompositeFiles and directories need uniform treatment despite structural differencesFileSystemNode/File/DirectorySkip only if the filesystem is provably flat forever (unrealistic for this prompt)
VisitorNew read-only aggregate operations keep arriving, and shouldn't force changes to node classesNodeVisitor, DiskUsageVisitor, SearchVisitorSkip if the operation set is genuinely closed and small — then a couple of direct methods are simpler and Visitor is overkill
IteratorDirectory traversal shouldn't leak the internal children storage structureDirectoryIteratorSkip if listing is always "materialize the whole list," with no need to abstract traversal itself
Proxy (optional)Permission checks or lazy content loading should sit in front of a node without changing its interfaceAn optional SecureFileSystemProxy wrapping FileSystemSkip if the round has no permission requirement at all

Explicitly avoided: Decorator for permission checks — see I for why Proxy is the semantically correct pattern (access control) even though the class shape looks similar to a Decorator (behavior augmentation).

N. What Is Likely to Change?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Storage backendIn-memory map-based treeReal disk-backed or cloud blob-store persistenceStorageBackend interface behind File.read()/write(), same DIP boundary as elsewhere in this document
Permission modelNone, or simple owner checkFull ACL / role-based accessProxy layer, not baked into FileSystemNode itself
SearchLinear SearchVisitor passIndexed search (e.g. trie by name, or an inverted index by extension)Search stays behind the same top-level FileSystem.search() signature regardless of internal implementation
LinksStrict one-parent treeSymbolic links (a node can be reachable from multiple paths)Would require revisiting the "single parent = no cycles" assumption entirely — flagged explicitly as a scope-expanding change, not a drop-in one

O. Design Evolution

What stays stable: FileSystem's top-level API signatures. New aggregate operations are pure additions (new visitor classes); persistence and permissions slot in behind existing interfaces without touching File/Directory.

P. API / Interface Design

interface NodeVisitor {
  void visitFile(File f);
  void visitDirectory(Directory d);
}

abstract class FileSystemNode {
  String name;
  Directory parent;
  abstract void accept(NodeVisitor v);
}

class FileSystem {
  void createFile(Path path);
  void createDirectory(Path path);
  void delete(Path path, boolean recursive);
  void move(Path source, Path destination);
  byte[] read(Path path);
  void write(Path path, byte[] data);
  List<String> list(Path directoryPath);
  List<Path> search(Path root, SearchCriteria criteria);
  long diskUsage(Path root);
}

Ownership/validation notes: move() resolves both the source and destination parent directories, verifies the destination is not a descendant of the source (cycle check), then acquires both parents' locks in canonical-path order before mutating either — never partially detach a node without knowing the reattachment will succeed.

Q. Pseudocode — Critical Logic

FileSystem.move(source, destination):
  srcParent = resolveParent(source)
  destParent = resolveParent(destination)
  node = srcParent.getChild(source.lastSegment)

  if node is Directory and destination.isDescendantOf(source):
    throw IllegalMoveException("cannot move a directory into its own subtree")

  // lock ordering: always acquire in canonical-path order to prevent
  // an ABBA deadlock against a concurrent move() going the other direction
  (first, second) = orderByCanonicalPath(srcParent, destParent)
  lock(first); lock(second)
  try:
    srcParent.removeChild(node.name)
    node.parent = destParent
    destParent.addChild(node)
  finally:
    unlock(second); unlock(first)

DiskUsageVisitor.visitDirectory(d):
  total = 0
  for child in d.children.values():
    child.accept(this)          // recurses; visitFile adds file.size directly
    total += this.lastSize
  this.lastSize = total

R. End-to-End Use Case

createDirectory("/docs")
createFile("/docs/report.txt"); write("/docs/report.txt", data)
createDirectory("/archive")
  │
  ▼
move("/docs/report.txt", "/archive/report.txt")
  │  resolveParent("/docs") and resolveParent("/archive")
  │  cycle check: "/archive" is not a descendant of "/docs/report.txt" ✓
  │  lock both parents in canonical order → detach → reattach → unlock
  ▼
diskUsage("/")
  │  DiskUsageVisitor recurses: "/docs" (0 bytes now) + "/archive" (report.txt's size)
  ▼
Total returned

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
Map<String, FileSystemNode> per DirectoryChild lookup by nameO(1) resolution of each path segmentA sorted structure (e.g. TreeMap) would give ordered listing for free at a small insert/lookup cost
List<String> segments in PathNormalized, comparable locationEnables the canonical-path ordering used for deadlock-free locking in move()A raw string path works but requires re-normalizing (resolving ../.) on every comparison
Readers-writer lock per nodeConcurrent reads, exclusive writes/structural changesMatches the actual access pattern — reads (list, read content) vastly outnumber writes in most filesystemsA single global lock is simpler but serializes unrelated subtrees needlessly at scale

T. Complexity Analysis

OperationTimeSpace
Path resolution (create/delete/read/write)O(depth of path)O(1) extra
diskUsage() / search()O(n) nodes in the subtree — no shortcut without caching or indexingO(depth) for recursion stack
move()O(depth of source) + O(depth of destination) for resolution and the cycle checkO(1)

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedPattern/Trade-off
"Add a `find files larger than X modified in the last week` query."Recognizing this as another Visitor, not a new node methodNew SearchVisitor with richer SearchCriteriaNew class onlyValidates the Visitor investment from M
"This needs to run across a cluster of machines, not one process."LLD/HLD boundary awarenessN/A at the LLD layer — candidate should name GFS/HDFS-style metadata-service + chunk-replication concerns and explicitly place them at HLDN/ARecognizing the boundary is the actual signal, not attempting to design a distributed FS live
"Two clients try to move overlapping subtrees at the same time — walk me through what happens."Deadlock and lock-ordering reasoningNone if canonical ordering is already in place — candidate should trace through itN/ADirectly exercises the W/Q lock-ordering design
"Add symbolic links."Testing whether the candidate recognizes this breaks the single-parent assumptionWould require general cycle detection instead of a simple ancestry check, and reference-counted targetsSignificant — new node type, revisit move/delete semanticsA strong candidate flags this as a real scope expansion, not a small add-on

W. Concurrency

Shared mutable state: every Directory's children map; every File's content.

X. Persistence

The prompt is explicitly in-memory, but the design should not hardcode that assumption into File itself: a StorageBackend interface behind read()/write() (in-memory byte array today, real disk or blob-store tomorrow) keeps persistence swappable without touching the tree structure, Composite hierarchy, or any visitor — the same DIP boundary used for payment gateways and cache backends elsewhere in this document.

Y. Scaling Boundary

LayerWhat Belongs Here
LLDComposite tree, Visitor operations, Iterator, per-node locking, lock-ordering protocol for move()
HLDDistributed metadata service, chunk/block servers, replication, consistency model across machines (GFS/HDFS/S3-style)
Distributed SystemsExplicitly named but out of scope for this round unless the interviewer pivots there directly — a strong candidate draws this line rather than either ignoring scale entirely or over-designing a distributed FS unprompted

Z. Failure Scenarios

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BRecommendation
Directory iteration semanticsFail-fast (throw if the directory changes mid-iteration)Snapshot (iterate a point-in-time copy)Fail-fast surfaces bugs immediately and matches Java's own collection convention; snapshot is friendlier for long-running reports (like disk-usage scans) that shouldn't abort because of unrelated concurrent writes — state the choice explicitly rather than leaving it implicit
Directory sizeCached, invalidated bottom-up on writesRecomputed on every diskUsage() callCache only if diskUsage() is called far more often than the tree is mutated; otherwise the invalidation bookkeeping costs more than it saves
Locking granularityPer-node locksPer-subtree (coarser) locksPer-node for realistic concurrent workloads; coarser locks are simpler to reason about but reintroduce the "unrelated operations block each other" problem the global lock has

AC. Interviewer Evaluation

Strong signal: reaching for Composite immediately and unprompted for the file/directory split; independently proposing Visitor once asked for a second read-only operation, with a clear ISP-based justification for why it's not just another node method; naming the lock-ordering fix for move() without being walked into the deadlock first.

Red flag: an isDirectory boolean surviving past the first design pass; adding every new query directly to the node interface; proposing a single global lock as the final answer rather than a deliberately simple starting point; not noticing the cycle risk in move() until explicitly prompted.

AD. Interview Simulation

Interviewer: Design an in-memory file system.
Candidate: Files and directories need to be treated uniformly by anything walking the tree, but a directory has children and a file doesn't — that's a textbook Composite: both extend a common FileSystemNode, and client code never needs to check which one it has.
Interviewer: Now add a way to compute total disk usage under any directory.
Candidate: I wouldn't add a size() method to every node type for this — there will always be another aggregate query after this one. I'd add a NodeVisitor interface and a DiskUsageVisitor implementation, dispatched through one stable accept() method on the nodes.
Interviewer: What about moving a file between two directories while other operations are happening concurrently?
Candidate: That needs locks on both the source and destination parent directories. If I lock them in call order, two concurrent moves crossing in opposite directions can deadlock — so I'd always acquire locks in a fixed canonical order, like comparing normalized path strings, regardless of which is logically the source.
Interviewer: What stops someone from moving a directory into its own subtree?
Candidate: An explicit ancestry check — is the destination a descendant of the source — done before any locks are taken, since it's a pure read that can reject the operation for free.

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
OfficialCTO — LLD: File SystemComposite-based File/Directory hierarchy with a shared abstract node; recursive size computation walked directly rather than via a separate visitor objectClean, correct Composite structure — validates this document's headline pattern choiceNo concurrency handling at all (single-threaded assumption throughout) and no Visitor abstraction — every new aggregate query would mean touching the node classes directly, which this document explicitly treats as an ISP violationThe base Composite shape itself; the concurrency and Visitor gaps are exactly what this document adds on top
AlgoMaster.io — Design an In-Memory File SystemNot independently verifiable — the page is paywalled/premium content behind a login, so only the course structure and pattern-name outline is publicly visiblePublicly visible outline lists Composite as the headline pattern, consistent with the approach hereCannot verify locking strategy, Visitor usage, or move-cycle handling beyond the outlineNothing beyond a weak cross-check that Composite is the expected headline pattern — cited honestly rather than fabricating unseen content

AF. Interview Cheat Sheet

ProblemFile System
DifficultyIntermediate → Advanced
Core requirementsCreate/delete/move files and directories, read/write content, list, search, disk usage
Key clarificationsIn-memory only? Permissions in scope? Multi-threaded? Symlinks? Search matching style?
Core entitiesFileSystemNode, File, Directory, Path, NodeVisitor, DirectoryIterator, FileSystem
Key relationshipsComposite unifies File/Directory; Visitor keeps aggregate queries off the node interface (ISP); Iterator abstracts traversal
Key patternsComposite (headline), Visitor, Iterator, optional Proxy for permissions
Most important decisionCanonical-path lock ordering for cross-directory move() to prevent deadlock
Likely-changing behaviorStorage backend, permission model, search implementation, link support
Concurrency concernReaders-writer lock per node; ABBA deadlock risk on move() without fixed lock ordering
Top follow-upsNew aggregate query, distributed-FS pivot (LLD/HLD boundary), concurrent-move deadlock trace, symlinks
Common mistakesisDirectory boolean instead of Composite; growing the base node interface per query instead of Visitor; call-order (not canonical-order) locking in move()
What NOT to over-engineerDon't design a distributed metadata/chunk-server system when the prompt says in-memory; don't add symlink/ACL complexity unless asked

AG. Full Implementation

A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.

// File System -- full LLD implementation with demo.
// Single-file Java translation of the Mermaid class diagram in section J.
// No concurrency badge on this problem -- kept single-threaded; the lock-ordering idea in the
// move() pseudocode is preserved only as commentary, not as real mutexes.
import java.util.*;

public class p9 {

    // ---------- Path (value object) ----------
    /** Normalized location, independent of the live tree. Says nothing about whether it exists. */
    static final class Path {
        final List<String> segments;
        Path(List<String> segments) { this.segments = List.copyOf(segments); }
        static Path of(String slashSeparated) {
            List<String> parts = new ArrayList<>();
            for (String s : slashSeparated.split("/")) if (!s.isEmpty()) parts.add(s);
            return new Path(parts);
        }
        Path parent() { return new Path(segments.subList(0, Math.max(0, segments.size() - 1))); }
        String lastSegment() { return segments.get(segments.size() - 1); }
        Path resolve(String childName) {
            List<String> next = new ArrayList<>(segments);
            next.add(childName);
            return new Path(next);
        }
        boolean isDescendantOf(Path other) {
            if (other.segments.size() >= segments.size()) return false;
            for (int i = 0; i < other.segments.size(); i++) {
                if (!other.segments.get(i).equals(segments.get(i))) return false;
            }
            return true;
        }
        public String toString() { return "/" + String.join("/", segments); }
    }

    // ---------- NodeVisitor (interface) ----------
    interface NodeVisitor {
        void visitFile(File f);
        void visitDirectory(Directory d);
    }

    // ---------- FileSystemNode (abstract) ----------
    /** Common identity for anything in the tree. Aggregate ops are delegated to a NodeVisitor. */
    static abstract class FileSystemNode {
        String name;
        Directory parent;
        FileSystemNode(String name, Directory parent) { this.name = name; this.parent = parent; }
        abstract void accept(NodeVisitor visitor);
    }

    // ---------- File (leaf) ----------
    static final class File extends FileSystemNode {
        byte[] content = new byte[0];
        File(String name, Directory parent) { super(name, parent); }
        int size() { return content.length; }
        byte[] read() { return content; }
        void write(byte[] data) { content = data; }
        void accept(NodeVisitor visitor) { visitor.visitFile(this); }
    }

    // ---------- Directory (composite) ----------
    static final class Directory extends FileSystemNode {
        final Map<String, FileSystemNode> children = new LinkedHashMap<>();
        Directory(String name, Directory parent) { super(name, parent); }
        void addChild(FileSystemNode node) { children.put(node.name, node); }
        void removeChild(String name) { children.remove(name); }
        FileSystemNode getChild(String name) { return children.get(name); }
        void accept(NodeVisitor visitor) { visitor.visitDirectory(this); }
    }

    // ---------- DirectoryIterator ----------
    /**
     * Snapshot iterator: copies the child list at creation time, so a concurrent addChild/removeChild
     * on the underlying Directory during iteration cannot throw ConcurrentModificationException. This
     * is a deliberate policy choice (snapshot over fail-fast) since the directory is a live, mutable map.
     */
    static final class DirectoryIterator implements Iterator<FileSystemNode> {
        private final List<FileSystemNode> snapshot;
        private int index = 0;
        DirectoryIterator(Directory directory) { this.snapshot = new ArrayList<>(directory.children.values()); }
        public boolean hasNext() { return index < snapshot.size(); }
        public FileSystemNode next() {
            if (!hasNext()) throw new NoSuchElementException();
            return snapshot.get(index++);
        }
    }

    // ---------- NodeVisitor implementations ----------
    static final class DiskUsageVisitor implements NodeVisitor {
        private long lastSize;
        long compute(FileSystemNode node) { node.accept(this); return lastSize; }
        public void visitFile(File f) { lastSize = f.size(); }
        public void visitDirectory(Directory d) {
            long total = 0;
            DirectoryIterator it = new DirectoryIterator(d);
            while (it.hasNext()) {
                FileSystemNode child = it.next();
                child.accept(this); // recurses; visitFile sets lastSize directly for leaves
                total += lastSize;
            }
            lastSize = total;
        }
    }

    interface SearchCriteria { boolean matches(FileSystemNode node); }

    static final class SearchVisitor implements NodeVisitor {
        private final SearchCriteria criteria;
        private final Path currentBase;
        private final List<Path> matches = new ArrayList<>();
        SearchVisitor(SearchCriteria criteria, Path base) { this.criteria = criteria; this.currentBase = base; }

        List<Path> search(FileSystemNode node) { walk(node, currentBase); return matches; }

        private void walk(FileSystemNode node, Path path) {
            if (criteria.matches(node)) matches.add(path);
            if (node instanceof Directory) {
                Directory d = (Directory) node;
                DirectoryIterator it = new DirectoryIterator(d);
                while (it.hasNext()) {
                    FileSystemNode child = it.next();
                    walk(child, path.resolve(child.name));
                }
            }
        }
        public void visitFile(File f) { /* traversal handled by walk(); visitor methods unused here */ }
        public void visitDirectory(Directory d) { /* see walk() */ }
    }

    // ---------- FileSystem (facade) ----------
    static final class FileSystem {
        final Directory root = new Directory("", null);

        private Directory resolveParent(Path path) {
            Directory current = root;
            for (int i = 0; i < path.segments.size() - 1; i++) {
                FileSystemNode next = current.getChild(path.segments.get(i));
                if (!(next instanceof Directory)) throw new IllegalArgumentException("No such directory: " + path);
                current = (Directory) next;
            }
            return current;
        }

        private FileSystemNode resolve(Path path) {
            if (path.segments.isEmpty()) return root;
            Directory parentDir = resolveParent(path);
            FileSystemNode node = parentDir.getChild(path.lastSegment());
            if (node == null) throw new IllegalArgumentException("No such path: " + path);
            return node;
        }

        void createFile(Path path) {
            Directory parentDir = resolveParent(path);
            parentDir.addChild(new File(path.lastSegment(), parentDir));
        }

        void createDirectory(Path path) {
            Directory parentDir = resolveParent(path);
            parentDir.addChild(new Directory(path.lastSegment(), parentDir));
        }

        void delete(Path path, boolean recursive) {
            Directory parentDir = resolveParent(path);
            FileSystemNode node = parentDir.getChild(path.lastSegment());
            if (node instanceof Directory && !((Directory) node).children.isEmpty() && !recursive) {
                throw new IllegalStateException("Directory not empty; pass recursive=true to delete: " + path);
            }
            parentDir.removeChild(path.lastSegment());
        }

        byte[] read(Path path) {
            FileSystemNode node = resolve(path);
            if (!(node instanceof File)) throw new IllegalArgumentException("Not a file: " + path);
            return ((File) node).read();
        }

        void write(Path path, byte[] data) {
            FileSystemNode node = resolve(path);
            if (!(node instanceof File)) throw new IllegalArgumentException("Not a file: " + path);
            ((File) node).write(data);
        }

        List<String> list(Path directoryPath) {
            FileSystemNode node = resolve(directoryPath);
            if (!(node instanceof Directory)) throw new IllegalArgumentException("Not a directory: " + directoryPath);
            List<String> names = new ArrayList<>();
            DirectoryIterator it = new DirectoryIterator((Directory) node);
            while (it.hasNext()) names.add(it.next().name);
            return names;
        }

        /**
         * Move source under destination's parent. Guards against moving a directory into its own
         * subtree (would create a cycle). No real threads exist in this single-threaded translation,
         * so the "lock in canonical-path order" step from the pseudocode is left as a comment only --
         * this is where a two-mutex ABBA-safe acquisition would go under a concurrent implementation.
         */
        void move(Path source, Path destination) {
            Directory srcParent = resolveParent(source);
            Directory destParent = resolveParent(destination);
            FileSystemNode node = srcParent.getChild(source.lastSegment());
            if (node == null) throw new IllegalArgumentException("No such path: " + source);

            if (node instanceof Directory && destination.isDescendantOf(source)) {
                throw new IllegalArgumentException("cannot move a directory into its own subtree");
            }

            // lock ordering would be established here by canonical path in a concurrent version
            srcParent.removeChild(node.name);
            node.name = destination.lastSegment();
            node.parent = destParent;
            destParent.addChild(node);
        }

        List<Path> search(Path root, SearchCriteria criteria) {
            FileSystemNode startNode = resolve(root);
            return new SearchVisitor(criteria, root).search(startNode);
        }

        long diskUsage(Path root) {
            FileSystemNode startNode = resolve(root);
            return new DiskUsageVisitor().compute(startNode);
        }
    }

    // ---------- Demo ----------
    public static void main(String[] args) {
        FileSystem fs = new FileSystem();
        fs.createDirectory(Path.of("/docs"));
        fs.createDirectory(Path.of("/docs/reports"));
        fs.createFile(Path.of("/docs/readme.txt"));
        fs.createFile(Path.of("/docs/reports/q1.csv"));
        fs.createFile(Path.of("/docs/reports/q2.csv"));
        fs.createDirectory(Path.of("/media"));

        fs.write(Path.of("/docs/readme.txt"), "hello world".getBytes());
        fs.write(Path.of("/docs/reports/q1.csv"), "a,b,c\n1,2,3".getBytes());
        fs.write(Path.of("/docs/reports/q2.csv"), "x,y,z\n4,5,6\n7,8,9".getBytes());

        System.out.println("Listing /docs: " + fs.list(Path.of("/docs")));
        System.out.println("Listing /docs/reports: " + fs.list(Path.of("/docs/reports")));

        System.out.println("Read /docs/readme.txt: " + new String(fs.read(Path.of("/docs/readme.txt"))));

        System.out.println("Disk usage of /docs: " + fs.diskUsage(Path.of("/docs")) + " bytes");

        List<Path> csvFiles = fs.search(Path.of("/docs"), node -> node instanceof File && node.name.endsWith(".csv"));
        System.out.println("Search *.csv under /docs: " + csvFiles);

        System.out.println("\nMoving /docs/reports -> /media/reports");
        fs.move(Path.of("/docs/reports"), Path.of("/media/reports"));
        System.out.println("Listing /media: " + fs.list(Path.of("/media")));
        System.out.println("Listing /docs after move: " + fs.list(Path.of("/docs")));

        try {
            fs.move(Path.of("/media"), Path.of("/media/reports/media"));
        } catch (IllegalArgumentException e) {
            System.out.println("\nExpected rejection moving a directory into its own subtree: " + e.getMessage());
        }

        try {
            fs.delete(Path.of("/media"), false);
        } catch (IllegalStateException e) {
            System.out.println("Expected rejection deleting non-empty dir without recursive: " + e.getMessage());
        }
        fs.delete(Path.of("/media"), true);
        System.out.println("Root listing after recursive delete of /media: " + fs.list(Path.of("")));
    }
}