bigonotes
CS ConceptsLLD

In-Memory File System

Design an in-memory file system as a tree of nodes with create, read, write, ls and delete on Unix-style paths.

4 min readLast updated LLD

An in-memory file system is a tree problem in disguise. Directories are nodes with children, files are leaves with content, and every operation starts by walking a path like /a/b/c from the root. Get one reusable traversal helper right and the rest is short.

Requirements#

Support the basic Unix-style operations:

  1. Create a file or directory at a path.
  2. Read and write a file's content.
  3. List a directory's contents.
  4. Delete a file or directory at a path.
  5. Paths are hierarchical, like /a/b/c.

Actor: the user (or calling program).

Clarifications

  • One kind of file (text content); a directory can hold many entries.
  • Failure cases: invalid path, missing parent, duplicate name, writing to a directory, creating or deleting the root.

Out of scope: UI, concurrency, permissions, persistence.

Core entities#

Two classes are enough:

Entity Holds
Node name, is_file, children: Dict[str, Node] (directories), content: str (files)
FileSystem root node

One Node type with an is_file flag keeps the code short. A directory uses children; a file uses content.

Relationships#

Everything hangs off the root as a tree:

  • FileSystem → root Node (composition).
  • Node → child Nodes (composition: deleting a directory deletes its subtree).

For example, after the demo below creates /a/b/f.txt, the tree looks like this:

  • / (dir)
    • a (dir)
      • b (dir)
        • f.txt (file, content "hello")

Key flows#

Every operation builds on one traversal helper.

Traverse(path): split on /, drop empty parts (so / → [] → root), then follow children one component at a time; return None if any step is missing.

Create(path): traverse to the parent → reject if missing, if the parent is a file, or if the name exists → add the new node.

Delete(path): traverse to the parent → remove the child by name (the whole subtree goes with it).

Code#

A single Node class plus the FileSystem that walks it:

from typing import Dict, List, Optional


class Node:
    def __init__(self, name: str, is_file: bool):
        self.name = name
        self.is_file = is_file
        self.children: Dict[str, "Node"] = {}  # used by directories
        self.content: str = ""                 # used by files


class FileSystem:
    def __init__(self):
        self.root = Node("/", is_file=False)

    def _components(self, path: str) -> List[str]:
        return [p for p in path.split("/") if p]  # "/a//b/" -> ["a", "b"]

    def _traverse(self, parts: List[str]) -> Optional[Node]:
        node = self.root
        for part in parts:
            if node.is_file or part not in node.children:
                return None
            node = node.children[part]
        return node

    def create(self, path: str, is_file: bool) -> bool:
        parts = self._components(path)
        if not parts:
            return False                      # can't create the root
        parent = self._traverse(parts[:-1])
        name = parts[-1]
        # Missing parent, parent is a file, or duplicate name
        if parent is None or parent.is_file or name in parent.children:
            return False
        parent.children[name] = Node(name, is_file)
        return True

    def read(self, path: str) -> Optional[str]:
        node = self._traverse(self._components(path))
        if node is None or not node.is_file:
            return None
        return node.content

    def write(self, path: str, content: str) -> bool:
        node = self._traverse(self._components(path))
        if node is None or not node.is_file:
            return False
        node.content = content
        return True

    def ls(self, path: str) -> Optional[List[str]]:
        node = self._traverse(self._components(path))
        if node is None:
            return None
        if node.is_file:
            return [node.name]  # ls on a file lists the file itself
        return sorted(node.children)

    def delete(self, path: str) -> bool:
        parts = self._components(path)
        if not parts:
            return False                      # can't delete the root
        parent = self._traverse(parts[:-1])
        if (parent is None or parent.is_file
                or parts[-1] not in parent.children):
            return False
        del parent.children[parts[-1]]        # removes the whole subtree
        return True


if __name__ == "__main__":
    fs = FileSystem()
    print(fs.create("/a", is_file=False))         # True
    print(fs.create("/a/b", is_file=False))       # True
    print(fs.create("/a/b/f.txt", is_file=True))  # True
    print(fs.create("/a/b/f.txt", True))          # False: duplicate
    print(fs.create("/x/y", False))               # False: parent missing

    print(fs.write("/a/b/f.txt", "hello"))        # True
    print(fs.read("/a/b/f.txt"))                  # hello
    print(fs.ls("/a/b"))                          # ['f.txt']
    print(fs.delete("/a/b/f.txt"))                # True
    print(fs.ls("/a/b"))                          # []

Extensions and trade-offs#

Common follow-ups and how the design absorbs them:

  • Separate File and Directory classes: a common Node base with two subclasses (the Composite pattern) removes the is_file checks and lets each type carry only its own fields. More classes, but cleaner invariants.
  • mkdir -p: create missing intermediate directories while traversing instead of failing.
  • Move/rename: detach the node from one parent's children and attach it to another; check you're not moving a directory into its own subtree.
  • Size and metadata: add size, timestamps and permissions to Node; directory size is the sum over its subtree.
  • Search: DFS from a node to find files by name or extension.
  • Concurrency: a single lock is simplest; finer-grained locking (per directory) is possible but tricky because operations span parent and child.

Key takeaways#

  • Model the file system as a tree; directories hold a name → Node map.
  • Write one traversal helper and reuse it in every operation.
  • Operations that change structure (create, delete) work on the parent of the target.
  • Guard the root and the "parent is a file" case explicitly.
  • The Composite pattern is the natural refactor to separate files and directories.

Found a mistake? Tell me on LinkedIn ↗

Further reading#