Back to LLD Problems: Parking Lot, Elevator, Chess, etc.
Q2Question 2 of 12

Design a File System

Key Concepts
  • The Composite pattern is the heart of the design: File and Directory share a common FileSystemNode interface.
  • A File is a leaf that returns its own byte size; a Directory is a composite that holds children and computes size recursively.
  • Path resolution walks the tree segment by segment from the root, descending through each named child.
  • Search is a recursive traversal matching a predicate; move re-parents a node; copy deep-clones a subtree.
  • Permissions (read, write, execute) are checked before every operation on each node.
  • Edge cases to guard: prevent cycles, reject moving a directory into its own descendant, make recursive deletes explicit.
  • At National Bank of India, the document archive uses the Composite pattern for nested customer folders and statements.

Definition

A file system is a recursive tree where directories and files share a common node interface via the Composite pattern.
  • FileSystemNode – abstract base with name, permissions, and operations like size() and delete().
  • File – a leaf node that stores content and returns its own byte size.
  • Directory – a composite that holds child nodes (files and sub-directories) and computes its size by summing children recursively.
  • Path resolution – walks the tree segment by segment from the root.
  • Permissions – read, write, execute flags checked before every operation.
  • Operations – move (re-parent), copy (deep-clone), delete (recursive for directories).

Think of National Bank of India's document archive. A folder can hold documents and more folders, which can hold yet more folders, endlessly nested. Rajesh's account folder contains statements and a sub-folder of tax documents. Whether you point at a single statement or a whole folder, you can ask its size or move it, because folders and documents share the same basic behaviour. That uniform treatment is the Composite pattern.

Understanding the Concepts

What is the core design?

The Composite pattern is the heart of the design. An abstract FileSystemNode declares operations that both files and directories support. A File is a leaf. A Directory is a composite that holds children.

JAVA
package com.bank.fs.model;

public abstract class FileSystemNode {
    private final String name;
    private Permission permission;
    private Directory parent;

    protected FileSystemNode(String name, Permission permission) {
        this.name = name;
        this.permission = permission;
    }

    public abstract long size();
    public abstract void delete();

    public String getName() { return name; }
    public Permission getPermission() { return permission; }
    public Directory getParent() { return parent; }
    public void setParent(Directory parent) { this.parent = parent; }

    public String getPath() {
        if (parent == null) return "/" + name;
        return parent.getPath() + "/" + name;
    }
}
JAVA
package com.bank.fs.model;

public class Permission {
    private final boolean read;
    private final boolean write;
    private final boolean execute;

    public Permission(boolean read, boolean write, boolean execute) {
        this.read = read;
        this.write = write;
        this.execute = execute;
    }

    public boolean canRead() { return read; }
    public boolean canWrite() { return write; }
    public boolean canExecute() { return execute; }
}

How does the File leaf work?

A File is a leaf that stores content and returns its own byte size.

JAVA
package com.bank.fs.model;

public class File extends FileSystemNode {
    private byte[] content;

    public File(String name, Permission permission, byte[] content) {
        super(name, permission);
        this.content = content;
    }

    @Override
    public long size() {
        return content.length;
    }

    @Override
    public void delete() {
        System.out.println("Deleted file: " + getPath());
    }

    public byte[] getContent() { return content; }
}

How does the Directory composite work?

A Directory holds child nodes and computes its size by summing its children recursively.

JAVA
package com.bank.fs.model;

import java.util.*;

public class Directory extends FileSystemNode {
    private final Map<String, FileSystemNode> children;

    public Directory(String name, Permission permission) {
        super(name, permission);
        this.children = new LinkedHashMap<>();
    }

    public void addChild(FileSystemNode node) {
        if (children.containsKey(node.getName())) {
            throw new IllegalStateException(
                    "Node " + node.getName() + " already exists");
        }
        node.setParent(this);
        children.put(node.getName(), node);
    }

    public void removeChild(String name) {
        children.remove(name);
    }

    public FileSystemNode getChild(String name) {
        return children.get(name);
    }

    @Override
    public long size() {
        return children.values().stream()
                .mapToLong(FileSystemNode::size)
                .sum();
    }

    @Override
    public void delete() {
        for (FileSystemNode child : new ArrayList<>(children.values())) {
            child.delete();
        }
        children.clear();
        System.out.println("Deleted directory: " + getPath());
    }

    public Collection<FileSystemNode> getChildren() {
        return children.values();
    }
}
TEXT
long total = directory.size(); // recurses over all children

How does path resolution work?

Path resolution walks the tree segment by segment from the root.

JAVA
package com.bank.fs.service;

import com.bank.fs.model.*;

public class FileSystemService {
    private final Directory root;

    public FileSystemService(Directory root) {
        this.root = root;
    }

    public FileSystemNode resolve(String path) {
        String[] segments = path.split("/");
        FileSystemNode current = root;
        for (String segment : segments) {
            if (segment.isEmpty()) continue;
            if (!(current instanceof Directory dir)) {
                throw new IllegalStateException(
                        segment + " is not a directory");
            }
            current = dir.getChild(segment);
            if (current == null) {
                throw new IllegalStateException(
                        "Not found: " + segment);
            }
        }
        return current;
    }
}

How does search work?

Search is a recursive traversal matching a predicate.

JAVA
public List<FileSystemNode> search(Directory dir, String namePattern) {
    List<FileSystemNode> results = new ArrayList<>();
    for (FileSystemNode child : dir.getChildren()) {
        if (child.getName().contains(namePattern)) {
            results.add(child);
        }
        if (child instanceof Directory subDir) {
            results.addAll(search(subDir, namePattern));
        }
    }
    return results;
}

How do move, copy, and delete work?

Move re-parents a node by removing it from its current directory and adding it to the target. Copy deep-clones a subtree. Delete on a directory is recursive.

JAVA
public void move(FileSystemNode node, Directory target) {
    if (isDescendant(node, target)) {
        throw new IllegalStateException(
                "Cannot move a directory into its own descendant");
    }
    node.getParent().removeChild(node.getName());
    target.addChild(node);
    System.out.println("Moved " + node.getName()
            + " to " + target.getPath());
}

private boolean isDescendant(FileSystemNode ancestor,
                              FileSystemNode node) {
    FileSystemNode current = node;
    while (current != null) {
        if (current == ancestor) return true;
        current = current.getParent();
    }
    return false;
}

What are the edge cases?

Edge cases are where a careful design shows:

  • Prevent cycles. A directory cannot contain itself. The move operation checks that the target is not a descendant of the node being moved.
  • Recursive deletes must be explicit. Deleting a non-empty directory recurses through all children.
  • Duplicate names. addChild rejects a child with a name that already exists in the directory.
  • Permissions. Every operation checks permissions before proceeding.

What should a senior engineer know?

  • The Composite pattern gives uniform treatment: whether a client points at a file or a folder, it calls the same size() or delete() method.
  • In production, file systems use B-trees or hash indexes for directory lookups, not linear maps.
  • Copy of large subtrees should be lazy (copy-on-write) for performance.
  • Real file systems handle hard links and symbolic links, which break the pure tree assumption.

What are common interview follow-up questions?

  • How would you add symbolic links?
  • How do you handle permissions inheritance from parent to child?
  • How would you implement a recycle bin (soft delete)?
  • What is the time complexity of computing a directory's size?
  • How would you optimise search for a file system with millions of files?

Visual Overview

DIAGRAM
  Composite Pattern: File System Tree
  ────────────────────────────────────

  root (Directory)
  ├── rajesh (Directory)
  │   ├── statements (Directory)
  │   │   ├── jan-2025.pdf (File, 45KB)
  │   │   └── feb-2025.pdf (File, 38KB)
  │   └── tax (Directory)
  │       └── form-16.pdf (File, 120KB)
  └── priya (Directory)
      └── loan-agreement.pdf (File, 95KB)

  root.size() = 45 + 38 + 120 + 95 = 298 KB
DIAGRAM
  FileSystemNode (abstract)
       │
       ├── File (leaf)
       │   size() → own bytes
       │
       └── Directory (composite)
           size() → sum of children.size()
           addChild(), removeChild()

Key Takeaway

When you design a file system, reach for the Composite pattern immediately, because a file system is fundamentally a recursive tree where a directory contains files and other directories without limit. Define an abstract file-system node with a name, permissions, and operations like size and delete. A File is a leaf that knows its own byte size, and a Directory is a composite that holds children and computes its size by summing its children recursively.

The power of this design is uniform treatment. Whether a client points at a single file or an entire folder, it calls the same size() or delete() operation, and the recursion handles arbitrary nesting automatically. Asking a top-level folder for its size naturally descends through every sub-folder and file beneath it.

The operations follow from the tree structure. Path resolution walks the tree segment by segment from the root. Search is a recursive traversal matching a predicate. Move re-parents a node by removing it from one directory and adding it to another. Copy deep-clones a subtree. Permissions are checked before every operation.

The edge cases are where a careful design shows. Prevent cycles so a directory can never contain itself. Make deletes on non-empty directories recursive and deliberate. Reject moving a directory into its own descendant because that would corrupt the tree. At National Bank of India, the document archive models customer folders nesting statements and tax sub-folders, and the Composite pattern lets the team report folder sizes and move documents cleanly regardless of depth.

Remember one sentence: a file system is a Composite-pattern tree where files are leaves, directories are composites, and every operation works uniformly regardless of nesting depth.

---

Share
Use ← → arrow keys to navigate