Courses › Low-Level Design › Problem Walkthroughs
Design an In-Memory File System
The Composite pattern, path resolution, move/remove edge cases and composable search.
"Design an in-memory file system" checks three things: whether you can model a tree of directories and files with the Composite pattern, whether you parse paths carefully, and whether the operations stay clean as the follow-ups pile up (move, size, search, permissions). The same model also underlies the *Unix find* and *cloud file storage* questions.
Requirements
mkdir(path)creates directories, including missing parents (likemkdir -p).write(path, content)creates or overwrites a file, andappendadds to it.read(path)returns the content.ls(path)lists a directory's entries in sorted order. For a file, it returns just the file's name.rm(path),mv(src, dst), andsize(path)(recursive for directories).- Errors: a missing path, a file where a directory was expected, removing a non-empty directory without the recursive flag.
Composite pattern
Files and directories share an interface (name, size(), parent), but only directories have children. Client code can treat both the same way, for example to compute a size recursively.
Node (abstract) name, parent, created_at; size(), path()
File content
Directory children: dict[name, Node]
FileSystem root; mkdir, write, read, ls, rm, mv, size, findStoring children in a dict keyed by name gives O(1) lookup for each path component, so resolving a path costs O(depth).
Implementation
class FSError(Exception):
pass
class Node:
def __init__(self, name, parent=None):
self.name, self.parent = name, parent
def path(self):
parts, node = [], self
while node.parent:
parts.append(node.name)
node = node.parent
return "/" + "/".join(reversed(parts))
class File(Node):
def __init__(self, name, parent, content=""):
super().__init__(name, parent)
self.content = content
def size(self):
return len(self.content)
class Directory(Node):
def __init__(self, name, parent=None):
super().__init__(name, parent)
self.children = {}
def size(self):
return sum(child.size() for child in self.children.values())
class FileSystem:
def __init__(self):
self.root = Directory("")
@staticmethod
def _parts(path):
if not path.startswith("/"):
raise FSError("paths must be absolute")
parts = []
for p in path.split("/"):
if p in ("", "."):
continue
if p == "..":
if parts:
parts.pop()
else:
parts.append(p)
return parts
def _resolve(self, path):
node = self.root
for name in self._parts(path):
if not isinstance(node, Directory) or name not in node.children:
raise FSError(f"no such file or directory: {path}")
node = node.children[name]
return node
def _parent_and_name(self, path, create_dirs=False):
parts = self._parts(path)
if not parts:
raise FSError("cannot operate on root")
node = self.root
for name in parts[:-1]:
child = node.children.get(name)
if child is None and create_dirs:
child = node.children[name] = Directory(name, node)
if not isinstance(child, Directory):
raise FSError(f"not a directory: {name}")
node = child
return node, parts[-1]
def mkdir(self, path):
node = self.root
for name in self._parts(path):
child = node.children.get(name)
if child is None:
child = node.children[name] = Directory(name, node)
elif not isinstance(child, Directory):
raise FSError(f"file exists: {name}")
node = child
def write(self, path, content, append=False):
parent, name = self._parent_and_name(path, create_dirs=True)
node = parent.children.get(name)
if isinstance(node, Directory):
raise FSError("is a directory")
if node is None:
parent.children[name] = File(name, parent, content)
else:
node.content = node.content + content if append else content
def read(self, path):
node = self._resolve(path)
if not isinstance(node, File):
raise FSError("is a directory")
return node.content
def ls(self, path="/"):
node = self._resolve(path)
if isinstance(node, File):
return [node.name]
return sorted(node.children)
def rm(self, path, recursive=False):
parent, name = self._parent_and_name(path)
node = parent.children.get(name)
if node is None:
raise FSError("no such file or directory")
if isinstance(node, Directory) and node.children and not recursive:
raise FSError("directory not empty")
del parent.children[name]
def mv(self, src, dst):
node = self._resolve(src)
if node is self.root:
raise FSError("cannot move root")
new_parent, new_name = self._parent_and_name(dst)
p = new_parent
while p: # moving a directory into itself
if p is node:
raise FSError("cannot move a directory into itself")
p = p.parent
if new_name in new_parent.children:
raise FSError("destination exists")
del node.parent.children[node.name]
node.name, node.parent = new_name, new_parent
new_parent.children[new_name] = node
def size(self, path):
return self._resolve(path).size()fs = FileSystem()
fs.write("/a/b/notes.txt", "hello")
fs.mkdir("/a/c")
assert fs.ls("/a") == ["b", "c"]
fs.mv("/a/b/notes.txt", "/a/c/todo.txt")
assert fs.read("/a/c/../c/todo.txt") == "hello"
assert fs.size("/a") == 5[!INTERVIEW] Name the edge cases you handle .. above the root, trailing slashes, writing to a path where a directory exists, moving a directory into its own subtree, and removing the root. Listing them, even before you implement them all, shows the carefulness this question is checking for.
Search: the find follow-up
"Find all files over 5 MB with the extension .log" is a DFS over the tree with a filter. Make the filters composable with the Specification pattern:
class Filter:
def matches(self, node): raise NotImplementedError
def __and__(self, other): return AndFilter(self, other)
def __or__(self, other): return OrFilter(self, other)
class AndFilter(Filter):
def __init__(self, a, b): self.a, self.b = a, b
def matches(self, node): return self.a.matches(node) and self.b.matches(node)
class OrFilter(Filter):
def __init__(self, a, b): self.a, self.b = a, b
def matches(self, node): return self.a.matches(node) or self.b.matches(node)
class ExtensionFilter(Filter):
def __init__(self, ext): self.ext = ext
def matches(self, node): return isinstance(node, File) and node.name.endswith(self.ext)
class MinSizeFilter(Filter):
def __init__(self, n): self.n = n
def matches(self, node): return isinstance(node, File) and node.size() >= self.n
def find(root, flt):
stack, out = [root], []
while stack:
node = stack.pop()
if flt.matches(node):
out.append(node.path())
if isinstance(node, Directory):
stack.extend(node.children.values())
return outNew criteria (owner, modified-after, name regex) are new Filter classes, and the search code doesn't change.
Follow-ups
- Fast size queries: cache each directory's size and update it up the parent chain on every write, append, remove or move. That's O(depth) per update instead of walking the whole subtree per query.
- Permissions and owners: add fields to
Node, and check them inside_resolve(execute permission on each directory) and in each operation. - Thread safety: a single read-write lock is the simple answer. Lock per directory is finer, but
mvthen needs both parents locked in a consistent order. - Symlinks: a third node type that holds a target path.
_resolvefollows it, with a hop limit to stop loops. - Large files: store content as a list of chunks or blocks instead of one string.
[!TIP] Practice In-Memory File System · File Search Tool (Unix find) · In-Memory Cloud File Storage