System Design
Object-Oriented Design
File System

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, and updatedAt.
  • File: A leaf node that stores data and has a size.
  • Directory: A container node that holds a list of Entry objects.

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?

  1. Simplicity: Clients can treat files and folders the same way (e.g., calling .getSize() on either).
  2. Extensibility: You can add new entry types (like Shortcut or HardLink) 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.