Design In-Memory File System

Last Updated: December 19, 2025

Ashish

Ashish Pratap Singh

hard

In this chapter, we will explore the low-level design of file system in detail.

Let's start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.

Here is an example of how a conversation between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • Support creation of files and directories organized in a hierarchical structure.
  • The entire file system state, including all files and directories, must be stored in memory.
  • Interaction with the file system will be through a shell that parses and executes string-based commands.
  • The following commands must be supported: mkdir, cd, touch, ls, pwd, cat, and echo?
  •  ls command should support a simple format and a detailed (-l) format.
  • The system must correctly handle both absolute (e.g., /home/user) and relative (e.g., documents, ../) paths.
  • Files should be able to store simple string-based content.

1.2 Non-Functional Requirements

  • Modularity: The system should follow object-oriented principles with clear separation of concerns across components.
  • Maintainability: Code should be clean, testable, and easy to extend or debug.
  • Extensibility: It should be easy to add new commands to the shell or new listing strategies for the ls command without modifying existing core logic.
  • Error Handling: The system should provide clear error messages for invalid operations, such as trying to cd into a file or accessing a non-existent path.
  • Usability: The system should expose an intuitive API for common file system operations.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., file, directory, path, metadata) and actions (e.g., create, read, move, list) from the functional requirements. These typically translate directly into classes, enums, or interfaces in an object-oriented design.

Let’s walk through the requirements and extract the relevant entities:

1. Support a hierarchical structure of files and directories.

This is the core data modeling challenge and is a perfect fit for the Composite design pattern.

  • FileSystemNode: An abstract base class that represents a generic entry in the file system (either a file or a directory). It defines common properties like name and parent.
  • File: The "Leaf" component in the Composite pattern. It represents an individual file and cannot contain other nodes. It has an additional property for content.
  • Directory: The "Composite" component. It can contain a collection of other FileSystemNodes (both Files and other Directorys), forming the tree structure.

2. Interact via a shell that executes commands like mkdir, cd, ls, etc.

This suggests encapsulating each user action into a distinct object, which is the essence of the Command design pattern.

  • Command (Interface): Defines a common execute method for all command objects.
  • Concrete Commands (MkdirCommand, CdCommand, LsCommand, etc.): Each class encapsulates all the information needed to perform a specific action, such as the target path and a reference to the file system instance.
  • Shell: Acts as the "Invoker" in the Command pattern. It is responsible for parsing the user's string input and creating the appropriate Command object to be executed.

3. The system must manage the entire file system state and the "current working directory".

A central manager is needed to hold the root of the file system tree and track the user's current location.

  • FileSystem: This class acts as the central "Receiver" for all commands and the manager of the entire file system state. It is implemented as a Singleton to ensure that there is only one instance of the file system in memory. It contains the logic for path resolution and all core file/directory manipulation operations.

These core entities define the key abstractions of the in-memory file system and will guide the structure of your low-level design and class diagrams.

3. Class Design

3.1 Class Definition

Shell

Shell

FileSystem

FileSystem
  • Inheritance: File and Directory extend FileSystemNode
  • Composition: A Directory contains a list of FileSystemNodes (files or subdirectories). This is a Composition relationship, forming the tree structure.
  • Association: FileSystem interacts with Directory and File objects to perform operations
  • FileSystem has a Directory: The main FileSystem class holds a reference to the root directory, which is the starting point of the entire hierarchy. This is also a Composition.

3.3 Key Design Patterns

Strategy Pattern

This pattern is used to define a family of algorithms, encapsulate each one, and make them interchangeable. It is applied to the ls command to support different output formats.

ListingStrategy
  • Strategy Interface: The ListingStrategy interface defines a common list() method.
  • Concrete StrategiesSimpleListingStrategy and DetailedListingStrategy provide different implementations for how to display the contents of a directory.
  • Context: The LsCommand is the context that is configured with a ListingStrategy object. When executed, it delegates the listing operation to its strategy.

The ls command is decoupled from the specific formatting logic. We can easily add new listing styles (e.g., a recursive listing strategy) by creating a new strategy class without any changes to the LsCommand or the core FileSystem logic.

Composite Pattern

This pattern is fundamental to modeling the hierarchical, tree-like structure of the file system. It allows us to treat individual objects (files) and compositions of objects (directories) uniformly.

Composite
  • Component: The FileSystemNode abstract class defines the common interface for all nodes in the tree.
  • Leaf: The File class represents a leaf node that cannot have children.
  • Composite: The Directory class represents a composite node that can contain other FileSystemNodes (both Files and other Directorys).

Command Pattern

To encapsulate a request (a shell command like mkdir or ls) as an object. This decouples the object that issues the request (Shell) from the object that knows how to perform it (FileSystem).

Command
  • Command Interface: The Command interface declares a single execute() method.
  • Concrete Commands: Classes like MkdirCommandCdCommand, and LsCommand implement the Command interface. Each object holds all the information needed for its execution, such as command arguments and a reference to the FileSystem.
  • Invoker: The Shell class acts as the invoker. It parses user input, creates the appropriate command object, and calls its execute() method.
  • Receiver: The FileSystem class is the receiver, containing the actual business logic that the commands execute.

This makes the system highly extensible. To add a new command (e.g., rm for remove), we only need to create a new Command implementation and add a case to the Shell's parsing logic, without modifying the FileSystem or any existing commands.

Facade Pattern

The FileSystem class acts as a Facade. It provides a simple, high-level API (mkdir, cp, ls) to the client, hiding the complex internal logic of path resolution, tree traversal, and concurrent locking.

3.4 Full Class Diagram

Class Diag

4. Implementation

4.1 FileSystemNode

An abstract base class representing both files and directories in the system.

1class FileSystemNode(ABC):
2    def __init__(self, name: str, parent: Optional['Directory']):
3        self.name = name
4        self.parent = parent
5        self.created_time = datetime.now()
6
7    def get_path(self) -> str:
8        if self.parent is None:  # This is the root directory
9            return self.name
10        # Avoid double slash for root's children
11        if self.parent.get_parent() is None:
12            return self.parent.get_path() + self.name
13        return self.parent.get_path() + "/" + self.name
14
15    def get_name(self) -> str:
16        return self.name
17
18    def set_name(self, name: str) -> None:
19        self.name = name
20
21    def get_parent(self) -> Optional['Directory']:
22        return self.parent
23
24    def get_created_time(self) -> datetime:
25        return self.created_time

It stores common properties like name, parent directory, and createdTime. It also provides a method getPath() to compute the absolute path of a node.

4.2 File (Leaf Component)

Represents a file in the filesystem.

1class File(FileSystemNode):
2    def __init__(self, name: str, parent: Optional['Directory']):
3        super().__init__(name, parent)
4        self.content = ""
5
6    def get_content(self) -> str:
7        return self.content
8
9    def set_content(self, content: str) -> None:
10        self.content = content

Inherits from FileSystemNode and adds file-specific data like content. Supports reading and writing content, but does not have child nodes.

4.3 Directory (Composite Component)

Represents a directory that can contain files or subdirectories.

1class Directory(FileSystemNode):
2    def __init__(self, name: str, parent: Optional['Directory']):
3        super().__init__(name, parent)
4        self.children: Dict[str, FileSystemNode] = {}
5        self._lock = Lock()
6
7    def add_child(self, node: FileSystemNode) -> None:
8        with self._lock:
9            self.children[node.get_name()] = node
10
11    def get_children(self) -> Dict[str, FileSystemNode]:
12        with self._lock:
13            return self.children.copy()
14
15    def get_child(self, name: str) -> Optional[FileSystemNode]:
16        with self._lock:
17            return self.children.get(name)

Extends FileSystemNode and maintains a collection of children nodes. Implements methods to add, fetch, and list child nodes, enabling the Composite pattern.

4.4 Command

An interface for all shell commands, following the Command design pattern.

1class Command(ABC):
2    @abstractmethod
3    def execute(self) -> None:
4        pass
5
6class CatCommand(Command):
7    def __init__(self, fs: 'FileSystem', path: str):
8        self.fs = fs
9        self.path = path
10
11    def execute(self) -> None:
12        content = self.fs.read_file(self.path)
13        if content is not None and content:
14            print(content)
15
16class CdCommand(Command):
17    def __init__(self, fs: 'FileSystem', path: str):
18        self.fs = fs
19        self.path = path
20
21    def execute(self) -> None:
22        self.fs.change_directory(self.path)
23
24class EchoCommand(Command):
25    def __init__(self, fs: 'FileSystem', content: str, file_path: str):
26        self.fs = fs
27        self.content = content
28        self.file_path = file_path
29
30    def execute(self) -> None:
31        # The '>' redirection character is handled implicitly by the command's nature.
32        # In a more complex shell, this would be more sophisticated.
33        self.fs.write_to_file(self.file_path, self.content)
34
35class LsCommand(Command):
36    def __init__(self, fs: 'FileSystem', path: Optional[str], strategy: 'ListingStrategy'):
37        self.fs = fs
38        self.path = path  # Path can be None, meaning "current directory"
39        self.strategy = strategy
40
41    def execute(self) -> None:
42        if self.path is None:
43            self.fs.list_contents(self.strategy)
44        else:
45            self.fs.list_contents_path(self.path, self.strategy)
46
47class MkdirCommand(Command):
48    def __init__(self, fs: 'FileSystem', path: str):
49        self.fs = fs
50        self.path = path
51
52    def execute(self) -> None:
53        self.fs.create_directory(self.path)
54
55class PwdCommand(Command):
56    def __init__(self, fs: 'FileSystem'):
57        self.fs = fs
58
59    def execute(self) -> None:
60        print(self.fs.get_working_directory())
61
62class TouchCommand(Command):
63    def __init__(self, fs: 'FileSystem', path: str):
64        self.fs = fs
65        self.path = path
66
67    def execute(self) -> None:
68        self.fs.create_file(self.path)

Each concrete command (mkdir, touch, cd, ls, pwd, cat, echo) encapsulates its own execution logic.

This decouples command execution from the Shell, making the system extensible for new commands.

4.5 ListingStrategy

Defines a strategy for displaying directory contents.

1class ListingStrategy(ABC):
2    @abstractmethod
3    def list(self, directory: Directory) -> None:
4        pass
5
6class SimpleListingStrategy(ListingStrategy):
7    def list(self, directory: Directory) -> None:
8        children = directory.get_children()
9        for name in children.keys():
10            print(name, end="  ")
11        print()
12
13class DetailedListingStrategy(ListingStrategy):
14    def list(self, directory: Directory) -> None:
15        children = directory.get_children()
16        for node in children.values():
17            node_type = 'd' if isinstance(node, Directory) else 'f'
18            print(f"{node_type}\t{node.get_name()}\t{node.get_created_time()}")

Two implementations are provided:

  • SimpleListingStrategy: prints only names.
  • DetailedListingStrategy: prints type (file/dir), name, and creation time.This follows the Strategy design pattern, allowing flexible listing behavior.

4.6 FileSystem (Client/Manager)

A singleton class managing the entire filesystem.

1class FileSystem:
2    _instance = None
3    _lock = Lock()
4
5    def __new__(cls):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10                    cls._instance._initialized = False
11        return cls._instance
12
13    def __init__(self):
14        if not hasattr(self, '_initialized') or not self._initialized:
15            self.root = Directory("/", None)
16            self.current_directory = self.root
17            self._initialized = True
18
19    @classmethod
20    def get_instance(cls):
21        return cls()
22
23    def create_directory(self, path: str) -> None:
24        self._create_node(path, True)
25
26    def create_file(self, path: str) -> None:
27        self._create_node(path, False)
28
29    def change_directory(self, path: str) -> None:
30        node = self._get_node(path)
31        if isinstance(node, Directory):
32            self.current_directory = node
33        else:
34            print(f"Error: '{path}' is not a directory.")
35
36    def list_contents(self, strategy: ListingStrategy) -> None:
37        strategy.list(self.current_directory)
38
39    def list_contents_path(self, path: str, strategy: ListingStrategy) -> None:
40        node = self._get_node(path)
41        if node is None:
42            print(f"ls: cannot access '{path}': No such file or directory", file=__import__('sys').stderr)
43            return
44
45        if isinstance(node, Directory):
46            strategy.list(node)
47        else:
48            # Mimic Unix behavior: if ls is pointed at a file, it just prints the file name.
49            print(node.get_name())
50
51    def get_working_directory(self) -> str:
52        return self.current_directory.get_path()
53
54    def write_to_file(self, path: str, content: str) -> None:
55        node = self._get_node(path)
56        if isinstance(node, File):
57            node.set_content(content)
58        else:
59            print(f"Error: Cannot write to '{path}'. It is not a file or does not exist.")
60
61    def read_file(self, path: str) -> str:
62        node = self._get_node(path)
63        if isinstance(node, File):
64            return node.get_content()
65        print(f"Error: Cannot read from '{path}'. It is not a file or does not exist.")
66        return ""
67
68    def _create_node(self, path: str, is_directory: bool) -> None:
69        print(self.current_directory.get_name())
70
71        if "/" in path:
72            # Path has directory components (e.g., "/a/b/c" or "b/c")
73            last_slash_index = path.rfind('/')
74            name = path[last_slash_index + 1:]
75            parent_path = path[:last_slash_index]
76
77            # Handle creating in root, e.g., "/testfile"
78            if not parent_path:
79                parent_path = "/"
80
81            parent_node = self._get_node(parent_path)
82            if not isinstance(parent_node, Directory):
83                print(f"Error: Invalid path. Parent '{parent_path}' is not a directory or does not exist.")
84                return
85            parent = parent_node
86        else:
87            # Path is a simple name in the current directory (e.g., "c")
88            name = path
89            parent = self.current_directory
90
91        if not name:
92            print("Error: File or directory name cannot be empty.", file=__import__('sys').stderr)
93            return
94
95        # --- Common logic from here ---
96        if parent.get_child(name) is not None:
97            print(f"Error: Node '{name}' already exists in '{parent.get_path()}'.")
98            return
99
100        new_node = Directory(name, parent) if is_directory else File(name, parent)
101        parent.add_child(new_node)
102
103    def _get_node(self, path: str) -> Optional[FileSystemNode]:
104        if path == "/":
105            return self.root
106
107        start_dir = self.root if path.startswith("/") else self.current_directory
108        # Use a non-empty string split to handle leading/trailing slashes gracefully
109        parts = path.split("/")
110
111        current = start_dir
112        for part in parts:
113            if not part or part == ".":
114                continue
115            if not isinstance(current, Directory):
116                return None  # Part of the path is a file, so it's invalid
117
118            if part == "..":
119                current = current.get_parent()
120                if current is None:
121                    current = self.root  # Can't go above root
122            else:
123                current = current.get_child(part)
124
125            if current is None:
126                return None  # Path component does not exist
127        return current
  • Keeps track of the root directory and the current working directory.
  • Provides methods for file/directory creation, navigation, reading, writing, and listing.
  • It also resolves paths and enforces filesystem rules (e.g., preventing duplicate names in a directory).

4.7 Shell

Acts as the command interpreter for the filesystem.

1class Shell:
2    def __init__(self):
3        self.fs = FileSystem.get_instance()
4
5    def execute_command(self, input_str: str) -> None:
6        parts = input_str.strip().split()
7        command_name = parts[0]
8
9        try:
10            if command_name == "mkdir":
11                command = MkdirCommand(self.fs, parts[1])
12            elif command_name == "touch":
13                command = TouchCommand(self.fs, parts[1])
14            elif command_name == "cd":
15                command = CdCommand(self.fs, parts[1])
16            elif command_name == "ls":
17                command = LsCommand(self.fs, self._get_path_argument_for_ls(parts), self._get_listing_strategy(parts))
18            elif command_name == "pwd":
19                command = PwdCommand(self.fs)
20            elif command_name == "cat":
21                command = CatCommand(self.fs, parts[1])
22            elif command_name == "echo":
23                command = EchoCommand(self.fs, self._get_echo_content(input_str), self._get_echo_file_path(parts))
24            else:
25                command = lambda: print(f"Error: Unknown command '{command_name}'.", file=__import__('sys').stderr)
26        except IndexError:
27            print(f"Error: Missing argument for command '{command_name}'.")
28            command = lambda: None  # No-op command
29
30        command.execute()
31
32    def _get_listing_strategy(self, args: List[str]) -> ListingStrategy:
33        if "-l" in args:
34            return DetailedListingStrategy()
35        return SimpleListingStrategy()
36
37    def _get_path_argument_for_ls(self, parts: List[str]) -> Optional[str]:
38        # Find the first argument that is not an option flag.
39        for part in parts[1:]:  # Skip the command name itself
40            if not part.startswith("-"):
41                return part
42        return None  # Return None if no path argument is found
43
44    def _get_echo_content(self, input_str: str) -> str:
45        # Simple parsing for "echo 'content' > file"
46        try:
47            start = input_str.index("'") + 1
48            end = input_str.rindex("'")
49            return input_str[start:end]
50        except ValueError:
51            return ""
52
53    def _get_echo_file_path(self, parts: List[str]) -> str:
54        # The file path is the last argument after the redirection symbol '>'
55        for i in range(len(parts)):
56            if parts[i] == ">" and i + 1 < len(parts):
57                return parts[i + 1]
58        return ""  # Should be handled by argument check

Parses user input (like mkdir /home) and maps it to the corresponding Command object. Supports options like ls -l, redirection (echo > file), and error handling for invalid commands or missing arguments.

4.8 FileSystemDemo

A demo/driver class that simulates a shell session. Validates the interaction of all components together.

1def main():
2    shell = Shell()
3    commands = [
4        "pwd",                          # /
5        "mkdir /home",
6        "mkdir /home/user",
7        "touch /home/user/file1.txt",
8        "ls -l /home",                  # d user
9        "cd /home/user",
10        "pwd",                          # /home/user
11        "ls",                           # file1.txt
12        "echo 'Hello World!' > file1.txt",
13        "cat file1.txt",                # Hello World!
14        "echo 'Overwriting content' > file1.txt",
15        "cat file1.txt",                # Overwriting content
16        "mkdir documents",
17        "cd documents",
18        "pwd",                          # /home/user/documents
19        "touch report.docx",
20        "ls",                           # report.docx
21        "cd ..",
22        "pwd",                          # /home/user
23        "ls -l",                        # d documents, f file1.txt
24        "cd /",
25        "pwd",                          # /
26        "ls -l",                        # d home
27        "cd /nonexistent/path"          # Error: not a directory
28    ]
29
30    for command in commands:
31        print(f"\n$ {command}")
32        shell.execute_command(command)
33
34if __name__ == "__main__":
35    main()

Runs a sequence of commands (e.g., mkdir, cd, ls, echo, cat) to demonstrate how the filesystem works end-to-end.

5. Run and Test

Files17
commands
composite
strategies
file_system_demo.py
main
file_system.py
shell.py
file_system_demo.pymain
Output

6. Quiz

Design In Memory File System Quiz

1 / 21
Multiple Choice

Which core entity in the in-memory file system design represents both files and directories in the hierarchy?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script