Design Example: In-Memory File System
Designing a File System is a great way to showcase your understanding of hierarchical data structures and the Composite Design Pattern.
Step 1: Requirements Gathering
Core Use Cases:
- Create files and directories.
- Delete files and directories.
- List the contents of a directory.
- Get the total size of a file or directory.
- Search for a specific file by name (recursive).
Clarifications:
- The system is in-memory (we aren't dealing with disk I/O).
- Directories can contain both files and other directories.
Step 2: Identify Core Objects
The key to this design is realizing that both File and Directory share common properties but behave differently.
- Entry (Abstract Base): Contains common fields like
name,createdAt, andupdatedAt. - File: A leaf node that stores data and has a size.
- Directory: A container node that holds a list of
Entryobjects.
Step 3: Design Class Diagram (Composite Pattern)
The Composite Pattern allows you to treat individual objects (File) and compositions of objects (Directory) uniformly.
Step 4: Implementation in TypeScript
abstract class Entry {
protected created: number;
constructor(protected name: string, protected parent: Directory | null) {
this.created = Date.now();
}
abstract getSize(): number;
public delete(): void {
if (this.parent) {
this.parent.removeEntry(this);
}
}
public getName(): string { return this.name; }
}
class FileEntry extends Entry {
constructor(name: string, parent: Directory | null, private content: string) {
super(name, parent);
}
public getSize(): number {
return this.content.length;
}
}
class Directory extends Entry {
private entries: Entry[] = [];
constructor(name: string, parent: Directory | null) {
super(name, parent);
}
public getSize(): number {
return this.entries.reduce((total, entry) => total + entry.getSize(), 0);
}
public addEntry(entry: Entry): void {
this.entries.push(entry);
}
public removeEntry(entry: Entry): void {
this.entries = this.entries.filter(e => e !== entry);
}
// Recursive Search
public find(name: string): Entry | null {
for (const entry of this.entries) {
if (entry.getName() === name) return entry;
if (entry instanceof Directory) {
const found = entry.find(name);
if (found) return found;
}
}
return null;
}
}Deep Dive: Composite Pattern
Why use the Composite Pattern here?
- Simplicity: Clients can treat files and folders the same way (e.g., calling
.getSize()on either). - Extensibility: You can add new entry types (like
ShortcutorHardLink) without changing the base logic.
Wrap Up
A File System design demonstrates your comfort with recursion and tree-based structures. It's a fundamental problem that appears frequently in senior engineer interviews.
[!TIP] When asked about searching, always specify if you are doing Breadth-First Search (BFS) or Depth-First Search (DFS). In most file systems, DFS is the natural fit for recursive structures.