File System LLD – Complete Low-Level Design with Classes & Architecture
Designing a file system is a classic Low-Level Design (LLD) interview question asked by top tech companies like Coinbase, Uber, and Confluent. At its core, a file system requires managing hierarchical data structures efficiently while allowing seamless extensibility for various operations (creation, reading, updating, deletion, and tree traversal).
This guide covers the full object-oriented architecture using the Composite Design Pattern combined with Trie-based path evaluation to build an interview-ready, modular file system.
1. Problem Statement & System Requirements
A file system organizes data into files and directories (folders) stored on a storage device in a hierarchical tree-like structure.
Core Functional Requirements
- Hierarchical Navigation: Support directory nesting with
/as the default path delimiter. - CRUD Operations:
- Create: Create directories or files at a specified absolute path (e.g.,
/document/cwl/design.txt). - Read: Retrieve node metadata or read content from a file path.
- Write: Set or update content inside a file.
- Delete: Remove a file or directory at a given path.
- Create: Create directories or files at a specified absolute path (e.g.,
- Hierarchy Display: Print/render the file system structure with visual indentation based on nesting depth.
- Path Validation: Handle invalid paths, duplicate names, and root-level parsing safely.
Core Non-Functional Requirements
- Extensibility: Easily support new file types (e.g., Symlinks, Executables, Compressed files) without refactoring the path resolution engine.
- Efficient Storage & Retrieval: Fast node lookup bounded by path depth , where is the number of path components, rather than total system nodes .
- Decoupled Architecture: High cohesion with low coupling using proven object-oriented design patterns.
2. Core Mental Model: The Composite Hierarchy & Trie Analogy
The Real-World Analogy
Think of a file system like a commercial office building:
- The Building contains Floors and Corridors (Directories).
- A Corridor can lead to smaller sub-corridors (Sub-directories) or terminate at an Office Room (File).
- Both corridors and office rooms have common properties (a room number/name, creation date, security access levels). However, while a corridor can contain other corridors and rooms, an office room is a terminal leaf that contains equipment/content.
Trie Structural Mapping
Standard file paths like /document/cwl/solid.java mirror a Trie (Prefix Tree) data structure:
- Root Node (
/): The starting Trie node. - Edges: Path delimiters (
/) splitting path string components into child transitions (documentcwlsolid.java). - Intermediate Nodes: Represent Directories.
- Leaf Nodes: Represent Files (or empty directories).
3. Architecture & Class Design
To handle files and directories uniformly while managing full sub-trees, we apply the Composite Design Pattern.
Class Roles & Responsibilities
-
FileSystemNode(Abstract Component):- Holds shared metadata:
name,createdDate,modifiedDate. - Declares abstract management operations like
display(int depth). - Acts as the unified Trie node type across the whole path structure.
- Holds shared metadata:
-
File(Leaf Node):- Represents terminal files containing data (
content,extension,fileSize). - Implements leaf-specific methods (
read(),write()). - Contains zero child nodes.
- Represents terminal files containing data (
-
Directory(Composite Node):- Manages a dynamic dictionary/map of child nodes (
Map<String, FileSystemNode> children). - Implements composite operations:
addChild(),removeChild(),getChild(). - Delegates recursive operations down through its children.
- Manages a dynamic dictionary/map of child nodes (
-
FileSystem/FileSystemManager(Central Controller):- Encapsulates root level operations (
root = new Directory("/")). - Implements Trie traversal logic for path splitting, validation, node fetching, path deletion, and content updates.
- Can optionally be wrapped inside a Singleton Pattern to enforce a single file system instance.
- Encapsulates root level operations (
4. Naive vs. Design Pattern Approach: Why Composite?
The Naive Anti-Pattern
Without the Composite pattern, developers typically create disjoint File and Directory classes, using explicit type checks (instanceof or enum Type { FILE, DIRECTORY }) throughout the code:
// NAIVE APPROACH (Anti-Pattern)
public class NaiveDirectory {
private String name;
private List<NaiveDirectory> subDirectories;
private List<NaiveFile> files;
public void printContents() {
for (NaiveDirectory dir : subDirectories) {
System.out.println("Dir: " + dir.getName());
// Deeply nested procedural recursion required!
}
for (NaiveFile file : files) {
System.out.println("File: " + file.getName());
}
}
}
Why this fails in interviews:
- Violates Open/Closed Principle (OCP): Adding new node types (e.g.,
SymLink,Shortcut) requires modifying every traversal method across the application. - Code Duplication: Operations like metadata updates, search, and deletion must be implemented twice (once for files, once for directories).
The Composite Pattern Solution
By abstracting both File and Directory under FileSystemNode, traversal logic treats every entity uniformly:
// CLEAN COMPOSITE APPROACH
public abstract class FileSystemNode {
protected String name;
public abstract void display(int depth);
}
5. Implementation Step 1: Base Component & Leaf Nodes (FileSystemNode & File)
Here is the production-ready Java implementation starting with the abstract component and leaf node.
import java.util.*;
/**
* Base abstract class representing any node within the file system tree.
* Acts as both the Composite Component and the underlying Trie Node.
*/
public abstract class FileSystemNode {
protected String name;
protected long createdTimestamp;
protected long modifiedTimestamp;
public FileSystemNode(String name) {
this.name = name;
this.createdTimestamp = System.currentTimeMillis();
this.modifiedTimestamp = System.currentTimeMillis();
}
public String getName() {
return name;
}
public long getModifiedTimestamp() {
return modifiedTimestamp;
}
public void updateModifiedTimestamp() {
this.modifiedTimestamp = System.currentTimeMillis();
}
/**
* Abstract method to render structure hierarchically.
* @param depth Current depth in tree used for formatting space indentation.
*/
public abstract void display(int depth);
}
/**
* Leaf Node: Represents an individual file containing content and extension metadata.
*/
public class File extends FileSystemNode {
private String content;
private String extension;
public File(String name) {
super(name);
this.content = "";
this.extension = extractExtension(name);
}
private String extractExtension(String name) {
int lastIndexOfDot = name.lastIndexOf('.');
if (lastIndexOfDot > 0 && lastIndexOfDot < name.length() - 1) {
return name.substring(lastIndexOfDot + 1);
}
return ""; // No extension found
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
updateModifiedTimestamp();
}
public String getExtension() {
return extension;
}
@Override
public void display(int depth) {
// Render indentation based on depth level (2 spaces per depth unit)
String indent = " ".repeat(depth);
System.out.println(indent + "- " + name + " [File, size: " + content.length() + " bytes]");
}
}
6. Implementation Step 2: Composite Node (Directory)
The Directory node manages child pointers using a dictionary to guarantee child lookups during path traversal steps.
/**
* Composite Node: Manages child FileSystemNodes (both directories and files).
*/
public class Directory extends FileSystemNode {
// Dictionary mapping child name to child FileSystemNode pointer
private final Map<String, FileSystemNode> children;
public Directory(String name) {
super(name);
this.children = new HashMap<>();
}
public Map<String, FileSystemNode> getChildren() {
return children;
}
public FileSystemNode getChild(String name) {
return children.get(name);
}
public void addChild(FileSystemNode node) {
children.put(node.getName(), node);
updateModifiedTimestamp();
}
public void removeChild(String name) {
children.remove(name);
updateModifiedTimestamp();
}
public boolean hasChild(String name) {
return children.containsKey(name);
}
@Override
public void display(int depth) {
String indent = " ".repeat(depth);
System.out.println(indent + "+ [" + name + "]");
// Polymorphically invoke display on all children
for (FileSystemNode child : children.values()) {
child.display(depth + 1);
}
}
}
7. Implementation Step 3: Central Controller & Trie Traversal (FileSystem)
The FileSystem manager performs Trie operations across absolute paths.
/**
* Central Controller executing file operations via Trie path operations.
*/
public class FileSystem {
private final Directory root;
public FileSystem() {
// Initialize root directory
this.root = new Directory("/");
}
/**
* Splits absolute path into components.
* Example: "/document/cwl/file.txt" -> ["document", "cwl", "file.txt"]
*/
private String[] parsePath(String path) {
if (path == null || path.isEmpty() || !path.startsWith("/")) {
return new String[0];
}
return Arrays.stream(path.split("/"))
.filter(component -> !component.isEmpty())
.toArray(String[]::new);
}
/**
* Creates directory paths or file leaf nodes dynamically.
*/
public boolean createPath(String path, boolean isFile) {
String[] components = parsePath(path);
if (components.length == 0) return false;
Directory current = root;
// Traverse down the Trie up to parent of the target node
for (int i = 0; i < components.length - 1; i++) {
String token = components[i];
if (!current.hasChild(token)) {
// Intermediate directory missing; create it automatically
Directory newDir = new Directory(token);
current.addChild(newDir);
current = newDir;
} else {
FileSystemNode child = current.getChild(token);
if (!(child instanceof Directory)) {
System.err.println("Path Error: " + token + " is a File, not a Directory.");
return false;
}
current = (Directory) child;
}
}
// Process the final leaf node
String targetName = components[components.length - 1];
if (current.hasChild(targetName)) {
System.out.println("Node already exists at target path.");
return false; // Prevent duplicates
}
FileSystemNode newNode = isFile ? new File(targetName) : new Directory(targetName);
current.addChild(newNode);
return true;
}
/**
* Retrieves node at path if present.
*/
public FileSystemNode getNode(String path) {
String[] components = parsePath(path);
if (components.length == 0) return root;
FileSystemNode current = root;
for (String token : components) {
if (!(current instanceof Directory)) {
return null; // Cannot traverse inside a file
}
current = ((Directory) current).getChild(token);
if (current == null) {
return null; // Path segment not found
}
}
return current;
}
/**
* Deletes a node at target path.
*/
public boolean deletePath(String path) {
String[] components = parsePath(path);
if (components.length == 0) return false; // Cannot delete root
Directory current = root;
for (int i = 0; i < components.length - 1; i++) {
String token = components[i];
FileSystemNode child = current.getChild(token);
if (!(child instanceof Directory)) {
return false;
}
current = (Directory) child;
}
String targetName = components[components.length - 1];
if (!current.hasChild(targetName)) {
return false;
}
current.removeChild(targetName);
return true;
}
public boolean setFileContent(String path, String content) {
FileSystemNode node = getNode(path);
if (node instanceof File file) {
file.setContent(content);
return true;
}
System.err.println("Error: Target path is not a file.");
return false;
}
public String getFileContent(String path) {
FileSystemNode node = getNode(path);
if (node instanceof File file) {
return file.getContent();
}
return null;
}
public void displayFileSystem() {
root.display(0);
}
}
8. Complete System Flow Walkthrough
Below is a step-by-step trace of how the system processes file creation, updates, and structural rendering.
9. Interview Variations, Edge Cases & Debugging Realities
Common Interview Edge Cases
-
Path Traversal Errors & Boundaries:
- Root Deletion: Attempting to invoke
deletePath("/"). Handled by returningfalseon empty component length. - Path Conflicts: Creating a directory where a file already exists with the same name (
/a/bas a file vs/a/b/cas a directory). Handled by explicitinstanceof Directorychecks during iteration. - Trailing / Redundant Delimiters: Handling double slashes gracefully (
/document//cwl///file.txt). Sanitized during parsing using.filter(c -> !c.isEmpty()).
- Root Deletion: Attempting to invoke
-
Algorithmic Complexity:
- Time Complexity:
- Path Creation/Lookup/Deletion: , where is path length / depth (number of path tokens). Lookups in each
DirectoryHashMaptake time on average. - Full Display Traversal: , where is total nodes under root.
- Path Creation/Lookup/Deletion: , where is path length / depth (number of path tokens). Lookups in each
- Space Complexity:
- Trie Storage: where represents the aggregate number of files and directories.
- Time Complexity:
-
Advanced Interview Follow-ups (e.g., Coinbase OA):
- File Search / Filter: How would you implement
search(Pattern pattern)?Solution: Implement a recursive visitor or traversal algorithm on
FileSystemNodematching names or extensions against regex patterns. - Symlinks / Shortcuts:
Solution: Create a
SymLinkNode extends FileSystemNodecontaining a target string referencetargetPath. Resolve target recursively during navigation up to a max jump depth (to prevent circular loops). - Permissions & Access Control:
Solution: Attach an
AccessControlList (ACL)toFileSystemNodechecking user permissions prior to executing read/write commands.
- File Search / Filter: How would you implement
10. Architectural Synthesis & Pattern Summary
Design Pattern Summary
Component Comparison Matrix
Summary and Key Takeaways
- Leverage Composite Pattern: Abstract files and directories behind a common interface (
FileSystemNode). This eliminates branchingif-elsetype-checking logic during tree traversals. - Treat Path Resolution as Trie Traversal: Deconstruct file paths by delimiter (
/) to traverse directory trees incrementally in time (where is path depth). - Decouple Storage from Control: Keep concrete data (
File,Directory) structurally separate from management engines (FileSystem). - Use Hash Maps for Sub-Directories: Store children in directories as key-value pairs (
Map<String, FileSystemNode>) for child lookups. - Handle File Extensions Explicitly: Parse extension metadata dynamically during node creation to easily support typed file operations.
- Protect Traversal Boundaries: Always validate path component types mid-traversal to prevent treating a leaf
Fileas a intermediateDirectory. - Design for Extensibility: A composite structure makes adding new features—such as symlinks, permissions, or custom display visitors—straightforward without rewriting core lookup logic.