Articles
File Explorer Coding Interview: Paths, Renames, and Cycles
Prepare for file explorer coding interviews with stable node IDs, derived paths, rename conflicts, move-cycle checks, and atomic operation test cases.
A file explorer coding interview is not finished when a recursive component draws folders. The harder task is keeping identity, ancestry, and user state consistent when a folder is renamed, a file moves, a sibling name conflicts, or an optimistic server response arrives out of order.
The essential rule is IDs identify nodes; paths describe their current location. Start with Low-Level Design of an In-Memory File System with mkdir and ls. The operation ledger below is an original virtual-filesystem exercise, not a reported employer prompt. It does not mutate real files or claim that client-side path checks secure an operating-system filesystem.
What filesystem does the interview mean?
Clarify whether the prompt is a virtual tree, a browser for server-managed files, or a tool that touches real disk paths. The distinction changes authorization, link handling, case sensitivity, concurrency, and error recovery. A clean UI tree model cannot silently stand in for operating-system semantics.
For this exercise, use one immutable root and a case-sensitive virtual namespace. Nodes are files or folders; symbolic links are not supported. Names must be nonempty and cannot contain /, or equal . or ... Sibling names must be unique, but the same name in different folders is allowed. The root cannot be renamed, moved, or deleted. These are declared exercise rules, not universal filesystem facts.
We implement create, rename, move, and subtree delete. Opening a file and selecting a row are separate UI actions. Copying files, permissions, file contents, remote child loading, and disk durability are extensions. Defining those limits early keeps a machine-coding answer runnable rather than spreading half-finished features across multiple abstractions.
Ask whether a move into an existing folder merges, rejects, or replaces. Our answer rejects a conflicting sibling name; it never silently merges subtrees. That choice makes the failure observable and avoids hiding destructive behavior behind a friendly drag-and-drop gesture.
Which state belongs to stable node IDs?
Represent each node as {id, parentId, name, kind}. Keep nodesById and an ordered childIdsByParentId index. Expansion, selection, keyboard focus, and pending operations refer to IDs. A displayed path is derived by walking parent links, not used as the permanent identity key.
Consider folder ID 7, initially named src, containing file ID 9, named readme.md. Renaming the folder to app changes /src/readme.md to /app/readme.md. IDs seven and nine remain unchanged, as do selection of file nine and expansion of folder seven. A path-keyed implementation may accidentally treat both as new nodes and lose UI state.
The graph needs consistency invariants: every non-root node has one existing folder parent; each parent's child list agrees with those parent links; an ID appears at most once in a parent's list; and following parents reaches root without a cycle. Unique sibling names are another invariant. Treat these as postconditions of every successful operation.
The two-index representation is convenient but introduces a maintenance obligation. Updating node.parentId without updating both child lists creates contradictory answers to “where is this file?” You can instead derive children by scanning nodes for a small exercise; that trades lookup cost for fewer mutable indexes. State the trade-off rather than pretending normalization is free.
How should paths be derived and looked up?
A path renderer walks from a node toward root, collects names, reverses them, and joins with the virtual separator. Include a visited-ID set so corrupt state cannot loop forever. A missing parent is an error, not an excuse to invent a partial path that looks valid.
For lookup, parse according to the declared virtual-path grammar and walk each segment through the current folder's children. Resolve /app/readme.md through root, app, then readme.md. Never use string replacement on all stored paths to implement a rename: /src and /src-old are distinct names, and descendants may be cached in several places.
Node's path documentation describes lexical path utilities and platform differences. Such utilities are useful when that platform's syntax is actually the contract, but normalization is not node identity or authorization. Our virtual model deliberately uses its own slash-separated rules and has no symbolic-link traversal.
If the prompt changes to real filesystem access, lexical normalization alone is insufficient evidence that a request is safely contained. The server needs an authorization and filesystem-resolution strategy appropriate to its platform, including links and concurrent changes. Node's filesystem documentation describes actual filesystem operations separately from lexical path utilities. Do not claim the ancestor walk below solves that different security problem.
Path derivation costs roughly the ancestor depth plus output length. Recomputing visible breadcrumbs is usually acceptable in a bounded interview tree. If caching is added, invalidate affected descendant paths after a folder rename or move. Keeping immutable IDs does not automatically make a path cache fresh.
What must rename validate before mutation?
Rename validates the target ID, root restriction, new name grammar, and sibling uniqueness. Compare only siblings under the same parent and exclude the node itself. Renaming readme.md to its current name is a no-op, not a collision with itself.
In our case-sensitive namespace, Report and report are different names. A case-insensitive extension would need an explicit comparison key and collision policy; applying toLowerCase() everywhere without documenting the rules is not enough. Preserve the user's display name separately if the namespace uses normalized comparison keys.
Use an inline rename editor with clear commit and cancel behavior. Enter commits once; Escape restores the old value without applying a mutation. Blur behavior must be specified: either commit a valid value or cancel, but avoid a second commit after Enter also causes blur. While editing, tree navigation shortcuts should not consume cursor movements inside the text field.
Validation failure retains the old node and provides a useful error. It must not temporarily rename the folder and then discover the collision, exposing inconsistent breadcrumbs or child indexes. The best first implementation separates a pure validation result from a single mutation step.
How do you reject a move that creates a cycle?
Moving Projects into its own Drafts descendant would make ancestry cyclic. Checking whether the destination's path starts with the source's path is fragile: names can change, string prefixes can confuse /a with /ab, and paths may be stale. Walk destination ancestors by stable ID instead.
function assertMoveAncestry(nodes, sourceId, destinationId) {
const seen = new Set();
let current = destinationId;
while (current !== null) {
if (current === sourceId) throw new Error('descendant destination');
if (seen.has(current)) throw new Error('corrupt parent cycle');
seen.add(current);
const node = nodes.get(current);
if (!node) throw new Error('missing ancestor');
current = node.parentId;
}
}
This is only the ancestry guard. Before calling it, validate both IDs, require a folder destination, and reject moving the root. After it, check destination name conflicts and any product-specific permissions. Only then change the source parent and the corresponding child indexes atomically.
Moving a folder into itself fails on the first iteration. Moving into a descendant fails when the walk reaches the source. Moving /a into sibling /ab can succeed because string resemblance is irrelevant. A preexisting corrupt cycle fails closed via seen rather than hanging the program.
The guard uses memory and time proportional to the destination's ancestor chain. A full subtree scan is not needed merely to determine ancestry in a valid single-parent tree. Explain that complexity after explaining correctness; a faster invalid operation is still invalid.
Can you defend the operation ledger?
Start with root, folders src and archive, and src/readme.md. The following original ledger describes acceptance outcomes, not an operating-system transcript:
| Operation | Expected outcome | Identity / consistency check |
|---|---|---|
| Rename src to app | Success | Folder and file IDs unchanged |
| Rename readme.md to readme.md | No-op | No self-collision |
| Create another readme.md under app | Reject | Existing file remains untouched |
| Create readme.md under archive | Success | Duplicate name allowed across parents |
| Move app into its descendant | Reject | No parent or child-list mutation |
| Move app into archive | Success if no conflict | Descendant paths change, IDs do not |
| Delete app subtree | Remove app and descendants | Selection/focus repaired to a survivor |
For subtree delete, collect all affected IDs before applying the mutation. Remove descendants, parent-index references, cached paths, expansion entries, and pending operations that refer to deleted nodes. If the selected file disappears, choose an explicit survivor or clear selection. Keyboard focus should land on a visible remaining row, often the surviving parent.
Our delete is an in-memory exercise operation, not an instruction to erase real files. A production product may require trash, confirmation, undo, or authorization. Do not hide those decisions in a low-level helper. A failed create or move likewise leaves the whole graph unchanged; checking only the returned error misses partial mutations.
After each ledger step, run a graph validator. It should verify both directions of the parent/child relationship, sibling uniqueness, reachability, and absence of cycles. These assertions detect bugs earlier than waiting for a recursive renderer to crash on the next expansion.
How should optimistic changes handle remote failure?
If a server owns the tree, send stable source and destination IDs with an operation ID and an expected revision. Paths may be useful display context, but a stale path should not identify a different node after a rename. The server is authoritative for authorization, conflict checks, and accepted state.
An optimistic UI can show the proposed change immediately, but its rollback must belong to that operation. Suppose rename operation one changes src to app; a later operation changes it to client; then operation one's failure arrives. Restoring a snapshot of the entire old tree would erase the later edit. A revision-aware inverse or targeted reconciliation avoids that clobbering.
For an interview, it is reasonable to choose a simpler policy: serialize mutations per node and disable conflicting edits while one is pending. Explain the cost in responsiveness and still handle unrelated-node edits. Alternatively, support concurrency with explicit revision checks. Do not claim a client-side timestamp establishes server ordering.
Show pending and failed states without changing identity. A failed move returns the node to an authoritative location or fetches current server state. A timeout is uncertainty, not proof that the server rejected the operation. Retry using the service's operation-deduplication contract rather than blindly repeating a potentially successful mutation.
What does keyboard accessibility add beyond the tree model?
The W3C Tree View pattern distinguishes focus from selection and defines navigation through tree items. Apply it only if implementing that composite widget. A simple nested disclosure list can use ordinary buttons instead, with a different Tab contract.
For a treeview, derive the visible preorder sequence from expansion state. Up and Down traverse that sequence, not every stored node. Right opens a closed folder or moves into its children; Left collapses or moves toward its parent under the pattern's conditions. A rename input temporarily owns text editing, so the tree handler must not intercept its keys as navigation.
Keep focus by node ID after reordering. If moving a selected node makes it invisible under a collapsed destination, repair focus to a visible row and explain whether selection persists. Expanding the destination automatically is another possible product choice, but should not happen accidentally because rendering follows a stale path key.
Our local tests cover virtual rename/move invariants, cycle rejection, unchanged state after failures, and revision-aware rollback decisions. They are not real-disk tests or a complete screen-reader compatibility suite. Pair those model checks with browser keyboard tests for the final tree widget rather than treating recursion as proof of interaction correctness.