Design an In-Memory File System
OOP design for an in-memory file system covering files and directories as a tree, path resolution, CRUD operations, search by name/extension, and permission management.
The Problem
Your company builds a cloud IDE that runs entirely in the browser. Users create projects with nested folder structures, edit files, and search across their workspace. The backend stores everything in a database, but the frontend needs a fast in-memory representation of the file tree so that operations like "open folder", "create file", and "find all .java files" feel instant.
A flat Map<String, byte[]> works for a prototype, but it breaks down fast. You cannot list a directory's children without scanning every key. Deleting a folder means finding every key that starts with that prefix. Permission checks require parsing paths on every operation. A tree structure with the Composite pattern solves all of these problems by making directories and files share a common interface while directories recursively contain children.
Design the core classes for an in-memory file system that supports nested directories, file CRUD operations, path resolution, recursive search, and permission management.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core operations, permission model, search capabilities, and boundaries.
You: "What operations does the file system need to support? Just read/write, or also move, rename, and delete?"
Interviewer: "Full CRUD. Create files and directories, read and write file content, delete entries, and move or rename them."
Six core operations. That is a meaty API surface, so the orchestrator class needs clean delegation to avoid bloat.
You: "How does path resolution work? Do we support absolute paths like /home/user/docs/readme.txt?"
Interviewer: "Yes. Paths are always absolute, starting with /. The root directory always exists. Path segments are separated by /."
Good. Path resolution means splitting the string by / and walking the tree from root to the target node. This is the spine of every operation.
You: "What kind of permission model do we need? Unix-style rwx per user, or something simpler?"
Interviewer: "Each entry has read, write, and execute permissions per user. Every mutating operation checks permissions before proceeding."
That means a Permission object per entry per user. Read access for viewing file content and listing directories. Write access for creating, modifying, and deleting. Execute for traversing directories during path resolution.
You: "Should search be recursive? If I search /home for .java files, does it search subdirectories too?"
Interviewer: "Yes, recursive by default. Support searching by name glob, by file extension, and by size range. Make the search criteria pluggable."
Strategy pattern for search. Each criterion (name, extension, size) implements the same interface, and callers compose them.
You: "Do we need symbolic links or hard links?"
Interviewer: "Not in the initial scope, but design the entry hierarchy so links can be added later."
Noted. We keep FileSystemEntry abstract so a SymbolicLink subclass can be introduced without changing existing code.
You: "Is there a maximum path depth or file size limit?"
Interviewer: "No hard limits. Assume in-memory is sufficient."
You: "Should file names be case-sensitive?"
Interviewer: "Yes. README.md and readme.md are different files."
Case-sensitive simplifies the implementation. Directory children can live in a Map<String, FileSystemEntry> keyed by exact name.
You: "Do we need to track metadata like creation time, modification time, and file size?"
Interviewer: "Yes. Track creation time, last modified time, and size in bytes for files."
Metadata lives on every FileSystemEntry. Files additionally track content size.
Final Requirements
Functional Requirements:
- Create files and directories at any valid path.
- Read file content and list directory children.
- Write (overwrite) file content, updating size and modification time.
- Delete files and directories (recursive delete for non-empty directories).
- Move and rename entries within the file system.
- Resolve absolute paths by walking the tree from root to target.
- Search recursively for entries matching pluggable criteria (name glob, extension, size range).
- Enforce read/write/execute permissions per user on every operation.
Non-Functional Requirements:
- Thread-safe for concurrent read and write operations.
- O(k) path resolution where k is the number of path segments.
- Extensible for new search criteria, entry types (symbolic links), and permission models.
Out of Scope:
- Persistence or database integration
- Symbolic links and hard links (designed for, not implemented)
- Disk quotas or storage limits
- File locking or transactional operations
- UI rendering or API endpoints
Example Inputs and Outputs
Scenario 1: Create nested structure and read
- Input: Create directory
/home, create directory/home/user, create file/home/user/notes.txtwith content "hello world". - Expected:
readFile("/home/user/notes.txt")returns "hello world".listDirectory("/home/user")returns["notes.txt"]. - Why: Validates path resolution, directory creation, file creation, and content retrieval.
Scenario 2: Recursive search by extension
- Input: File system contains
/src/Main.java,/src/util/Helper.java,/src/README.md,/docs/guide.txt. - Expected:
search("/src", extensionCriteria(".java"))returns["/src/Main.java", "/src/util/Helper.java"]. The.mdand.txtfiles are excluded. - Why: Validates recursive directory traversal and pluggable search criteria.
Scenario 3: Permission denied on write
- Input: User "alice" has read-only permission on
/home/bob/secret.txt. Alice callswriteFile("/home/bob/secret.txt", "hacked"). - Expected: Operation throws
PermissionDeniedException. File content remains unchanged. - Why: Validates permission checking before mutating operations.
Try It Yourself
Try it yourself
Before reading the solution, spend 15-20 minutes sketching your own class diagram. Focus on how files and directories share a common interface (Composite pattern), how path resolution walks the tree, and where permissions get checked. Think about whether search belongs on entries or on a separate service. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look for nouns in the requirements. You find entries (files and directories), paths, permissions, users, search criteria, and the file system itself. Each one maps to a class with a single clear responsibility.
A common mistake is putting everything into one giant FileSystem class that handles path parsing, permission checking, content storage, and search. Good design means each class has one job. The file system orchestrates, entries hold data and children, permissions gate access, and search criteria filter results.
| Entity | Responsibility | Key attributes |
|---|---|---|
| FileSystemEntry | Abstract base for anything in the tree. Holds metadata and permissions. | name, createdAt, modifiedAt, parent, permissions |
| File | Leaf node. Stores content bytes and tracks size. | content, size |
| Directory | Composite node. Contains a map of child entries. | children (Map of name to entry) |
| Path | Value object. Parses and normalizes absolute path strings. | segments (List of String) |
| Permission | Tracks read/write/execute flags for a single user on a single entry. | canRead, canWrite, canExecute |
| User | Identity for permission checks. | username |
| SearchCriteria | Strategy interface for filtering entries during recursive search. | (strategy interface) |
| FileSystem | Orchestrator. Owns the root directory, resolves paths, delegates operations. | root, currentUser |
Notice that File and Directory both extend FileSystemEntry. This is the Composite pattern: a directory contains a list of FileSystemEntry references, each of which can be either a File or another Directory. This recursive structure models the tree naturally.
Path is a separate value object rather than a raw String because path parsing (splitting by /, validating no empty segments) is logic that deserves its own home. Every method that accepts a path works with a parsed Path object, not a raw string.
Step 2: Define Relationships and Class Design
FileSystem
The orchestrator. It owns the root directory, resolves paths, checks permissions, and delegates every operation to the targeted entry.
Deriving state from requirements:
| Requirement | What FileSystem must track |
|---|---|
| "Nested directory structure" | The root Directory node |
| "Permission checks per user" | The current User |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Create files at any path" | createFile(path, content) |
| "Create directories" | createDirectory(path) |
| "Read file content" | readFile(path) |
| "Write file content" | writeFile(path, content) |
| "Delete entries" | delete(path) |
| "Move and rename" | move(source, dest), rename(path, newName) |
| "List directory children" | listDirectory(path) |
| "Recursive search" | search(path, criteria) |
| "Path resolution" | resolvePath(path) (private) |
Directory
The composite node. It contains children (files or subdirectories) in a map keyed by name. Adding, removing, and looking up children are all O(1) operations.
Deriving state from requirements:
| Requirement | What Directory must track |
|---|---|
| "Directories contain files and subdirectories" | Map of name to FileSystemEntry |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Add file/directory to parent" | addChild(entry) |
| "Delete child by name" | removeChild(name) |
| "Path resolution walks children" | getChild(name) |
| "List directory contents" | listChildren() |
Key relationship decisions:
- Directory extends FileSystemEntry (Composite pattern). A directory IS an entry, which means directories can contain other directories. This recursive structure models the tree.
- FileSystemEntry holds permissions as a
Map<String, Permission>keyed by username. This co-locates access control with the data it protects. - Path is a separate value object, not just a String. It parses, validates, and normalizes the path once instead of re-parsing on every operation.
Step 3: Choose Design Patterns
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.