In-memory File System
Problem statement
Design an in-memory file system supporting mkdir -p, ls, addContentToFile, readContentFromFile, rm, mv, find by name/size, permissions and symlinks (optional), with paths like /a/b/c.
Requirements
Functional
Create directories (recursively), create/append/read files.
List a directory (sorted) or return a file name.
Delete, move/rename, copy.
Find by name pattern/extension/size (with pluggable filters).
Optional: permissions, symlinks, quotas.
Non-functional
Path resolution O(depth); ls O(k log k) for k children.
Thread-safe for concurrent readers and writers (per-directory locking).
Composite structure: files and directories share an interface.
API
void mkdir(String path) // -p semantics
List<String> ls(String path) // sorted names, or [fileName]
void addContentToFile(String path, String content)
String readContentFromFile(String path)
void rm(String path, boolean recursive)
void mv(String from, String to)
List<String> find(String path, Filter f)
Data model
Node(abstract): name, parent, createdAt, modifiedAt, permissions →File(content: StringBuilder / byte[] chunks, size),Directory(TreeMap<String, Node>children),SymLink(target).FileSystem: rootDirectory,resolve(path) → Node,resolveParent(path) → (Directory, name).Filter(interface) for find:NameFilter,ExtensionFilter,SizeFilter,AndFilter.Pathhelper: normalise, split, handle./...
Design
- Composite pattern:
DirectoryandFileboth extendNode; directories hold children in aTreeMapsolsis sorted automatically. - Path resolution: split on
/, walk from root;resolveParentreturns the directory and last segment for create/move/delete. Normalise.and..(never above root). - mkdir -p: walk, creating missing directories; error if a segment exists as a file.
- Files as chunked buffers (
List<byte[]>) so append is O(1) amortised and reads don't copy the whole content. - Find: DFS from a directory applying a
Filtercomposite (AND/OR/NOT) — Specification pattern. - Concurrency:
ReadWriteLockper directory; lock ordering by path depth to avoid deadlocks inmv(lock both parents in canonical order). - mv = detach node from old parent, rename, attach to new parent; reject moving a directory into its own subtree.
Class diagram
Deep dives
TreeMap vs HashMap for children: sorted ls for free at O(log k) insert; HashMap + sort on ls is fine too — state the trade-off.
Directory size = sum of children (cache with invalidation up the parent chain on writes, or compute lazily).
Symlinks: resolve with a hop limit to avoid loops.
Why parent pointers: needed for .., mv, and recomputing sizes; keep them consistent in add/remove.
Bottlenecks & mitigations
Directory size recomputed recursively on every query → cached sizes with invalidation up the tree.
Huge directories →
lsO(k log k); paginate.Global lock → per-directory RW locks; lock in path order.
Failure modes
- File and directory name clash → error on mkdir over a file or write into a directory.
- Move into own subtree → cycle check up the parent chain.
- Concurrent rm and read → readers hold a read lock on the parent; rm takes a write lock.
- Path normalisation edge cases (
//, trailing/,..above root) → normalise centrally.
Code skeleton
from threading import RLock
class Node:
def __init__(self, name, parent=None): self.name, self.parent = name, parent
class File(Node):
def __init__(self, name, parent): super().__init__(name, parent); self.chunks = []
def append(self, s): self.chunks.append(s)
def read(self): return "".join(self.chunks)
def size(self): return sum(len(c) for c in self.chunks)
class Directory(Node):
def __init__(self, name, parent=None): super().__init__(name, parent); self.children, self.lock = {}, RLock()
def size(self): return sum(c.size() for c in self.children.values())
class FileSystem:
def __init__(self): self.root = Directory("")
def _parts(self, path): return [p for p in path.split("/") if p and p != "."]
def _resolve(self, path, create=False):
node = self.root
for p in self._parts(path):
if p == "..": node = node.parent or node; continue
if not isinstance(node, Directory): raise FileNotFoundError(path)
with node.lock:
if p not in node.children:
if not create: raise FileNotFoundError(path)
node.children[p] = Directory(p, node)
node = node.children[p]
return node
def mkdir(self, path): self._resolve(path, create=True)
def ls(self, path):
n = self._resolve(path)
return sorted(n.children) if isinstance(n, Directory) else [n.name]
def add_content(self, path, content):
parts = self._parts(path); d = self._resolve("/" + "/".join(parts[:-1]), create=True)
with d.lock:
f = d.children.get(parts[-1])
if f is None: f = d.children[parts[-1]] = File(parts[-1], d)
if isinstance(f, Directory): raise IsADirectoryError(path)
f.append(content)
def read(self, path):
f = self._resolve(path)
if isinstance(f, Directory): raise IsADirectoryError(path)
return f.read()
def rm(self, path, recursive=False):
n = self._resolve(path)
if isinstance(n, Directory) and n.children and not recursive: raise OSError("directory not empty")
with n.parent.lock: del n.parent.children[n.name]
def mv(self, src, dst):
n = self._resolve(src); parts = self._parts(dst); dparent = self._resolve("/" + "/".join(parts[:-1]))
cur = dparent
while cur:
if cur is n: raise OSError("cannot move into itself")
cur = cur.parent
with n.parent.lock: del n.parent.children[n.name]
n.name, n.parent = parts[-1], dparent
with dparent.lock: dparent.children[n.name] = n
def find(self, path, pred):
out, stack = [], [(self._resolve(path), path.rstrip("/") or "")]
while stack:
n, p = stack.pop()
if pred(n): out.append(p or "/")
if isinstance(n, Directory):
for name, c in n.children.items(): stack.append((c, f"{p}/{name}"))
return sorted(out)
Follow-up questions interviewers ask
Add permissions (rwx per owner/group) and an access check on every op.
Add a journal so the FS can be persisted and replayed.
Add quotas per directory subtree.