Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design)  Module 16 – Case studies: infrastructure components

Low-level design: in-memory file system

Design an in-memory file system in code: files and folders as a Composite, POSIX path resolution, moves that refuse their own subtree, and find-style search.

  • Intermediate
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot
Free intro, Pro: 8 of 35 minutes free
Free: The prompt, the requirements and the first design. With a pass: the other 11 sections and 3 interview answers, about 27 minutes.

What you will learn

  • Model files, directories and paths with the Composite pattern
  • Resolve absolute and relative paths with '.', '..' and repeated slashes
  • Implement mkdir, write, read, ls, mv and rm so that a failed call changes nothing
  • Search files with composable filters, as the find utility does

Before you start

What you’ll learn · Before you start

What you will learn

  • Model files, directories and paths with the Composite pattern
  • Resolve absolute and relative paths with '.', '..' and repeated slashes
  • Implement mkdir, write, read, ls, mv and rm so that a failed call changes nothing
  • Search files with composable filters, as the find utility does

Before you start

On this page

Designing an in-memory file system is a low-level design (LLD) exercise in classes: hold a tree of directories and files in memory, and run a small shell over it that makes directories, writes and reads files, lists, moves, removes and searches. Five decisions carry the model in this case study. Files and directories share one interface, the Composite pattern, so a size or a search treats a whole tree and a single file alike. A directory maps each child’s name to the child, so a name lives in exactly one place. Every operation turns its path into nodes with one resolver that walks the tree component by component, as POSIX describes pathname resolution. A move checks everything before it changes anything, and never puts a directory inside itself. And a search is built from small filter objects, the way find combines its tests.

The prompt, the requirements and the first design

The prompt: “Design an in-memory file system with a shell-like API.” Four questions shape the answer. One user, or permissions too? Paths relative to a current directory? Links? Several threads? Here the answers are one user, both kinds of path, no links yet and one thread first, and they become the requirements:

  • F1 mkdir(path) creates a directory; with parents, also the missing ones above it, like mkdir -p.
  • F2 write(path, text) creates or replaces a file, read(path) returns its text and ls(path) lists names.
  • F3 mv(src, dst) renames or moves, into dst when it is a directory, never into the moved directory itself.
  • F4 rm(path) removes a file or an empty directory; recursive removes a whole tree.
  • F5 find(path, filter) lists the paths below path that match tests of name, type, size and age.
  • N1 Paths are absolute or relative; ., .. and repeated slashes mean what POSIX says.
  • N2 A failed call changes nothing and names its error as POSIX does: ENOENT, ENOTDIR, EINVAL.
  • N3 Time comes from a clock the file system is given, so tests choose it.

N1 decides more than it seems. A tempting shortcut cleans the path as a string, as Python’s posixpath.normpath() does, and only then looks it up. The run below resolves eight paths from /home/asha, where notes.txt is a file and src a directory, both ways:

Eight paths cleaned as strings and walked through the tree Python · paths/lexical.py
"""Cleaning a path as a string, against walking it through the tree (lesson lld/in-memory-file-system).

posixpath.normpath() removes repeated slashes, "." and "name/.." by looking at the string alone. The resolver of
this lesson walks the tree one component at a time, as POSIX pathname resolution does, so it notices when a name
is missing or is a file. The tree is a dict of dicts, a str is a file's content, and the current directory is
/home/asha."""

import posixpath

TREE = {"home": {"asha": {"notes.txt": "buy milk", "src": {"app.py": "print('hi')"}}}, "tmp": {}}
CWD = ["home", "asha"]


def node_at(names):
    node = TREE
    for name in names:
        node = node[name]
    return node


def walk(path):
    """The absolute path that path names, or the error resolution stops with."""
    names = [] if path.startswith("/") else list(CWD)
    node = node_at(names)
    for part in path.split("/"):
        if not part:  # repeated, leading and trailing slashes
            continue
        if not isinstance(node, dict):
            return f"ENOTDIR: /{'/'.join(names)} is a file"
        if part == "..":
            names = names[:-1]  # at the root, ".." is the root
        elif part != ".":
            if part not in node:
                return f"ENOENT: /{'/'.join(names)} has no {part}"
            names.append(part)
        node = node_at(names)
    if path.endswith("/") and not isinstance(node, dict):
        return "ENOTDIR: a trailing slash needs a directory"
    return "/" + "/".join(names)


PATHS = [
    "/home//asha/./src/",
    "src/../notes.txt",
    "/../../tmp",
    "notes.txt/..",
    "missing/../notes.txt",
    "src/app.py/../../notes.txt",
    "notes.txt/",
    "//tmp",
]

print(f"{'path':28} {'cleaned as a string':22} walked through the tree")
differ = 0
for path in PATHS:
    cleaned = posixpath.normpath(posixpath.join("/home/asha", path))
    walked = walk(path)
    differ += cleaned != walked
    print(f"{path:28} {cleaned:22} {walked}")
print(f"{differ} of {len(PATHS)} paths get a different answer from the string and from the tree")

Output

path                         cleaned as a string    walked through the tree
/home//asha/./src/           /home/asha/src         /home/asha/src
src/../notes.txt             /home/asha/notes.txt   /home/asha/notes.txt
/../../tmp                   /tmp                   /tmp
notes.txt/..                 /home/asha             ENOTDIR: /home/asha/notes.txt is a file
missing/../notes.txt         /home/asha/notes.txt   ENOENT: /home/asha has no missing
src/app.py/../../notes.txt   /home/asha/notes.txt   ENOTDIR: /home/asha/src/app.py is a file
notes.txt/                   /home/asha/notes.txt   ENOTDIR: a trailing slash needs a directory
//tmp                        //tmp                  /tmp
5 of 8 paths get a different answer from the string and from the tree

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lexical.py

Five answers differ. notes.txt/.. names nothing, because .. needs a directory before it; the string version answers /home/asha anyway. missing/../notes.txt fails because missing does not exist, and notes.txt/ because a trailing slash names a directory. The last line is a choice, not a bug: POSIX lets an implementation give exactly two leading slashes a meaning of its own, and this design reads them as the root.

Class diagram: File and Directory share the Node interface, a directory owns its children by name, and find() asks composable filters.FileSystem+root, cwdDirectory+clock() -> seconds+resolve(path)Node+mkdir(), write(), read(), ls(), cd()+mv(src, dst), rm(path)+find(path, match, skip)pathsDirectory+childrenname -> NodeFile+datatextNode+namestr+parentDirectory+modifiedseconds+path()str+size()bytesFilter+matches(node, now)boolName, Type, Size, Modified+valuepattern, f or d, n[c], daysAnd, Or, Not+operands1 or 2 Filtersroot, cwdchildren10..*find() asks

The in-memory file system's class diagram

Text description of the diagram

The diagram shows seven boxes in UML notation. Each box names a class, then lists its attributes and, below a line, its operations.

  • FileSystem holds the root and the current directory, both Directories, and a clock that returns seconds. Its operations are resolve(path), returning a Node; mkdir, write, read, ls and cd; mv(src, dst) and rm(path); and find(path, match, skip), returning paths. An arrow labelled "root, cwd" leads to Directory.
  • Node has a name, a parent Directory and a modified time, and the operations path(), returning a string, and size(), returning bytes.
  • File and Directory each point to Node with a hollow triangle: both are kinds of Node. File adds its data, as text. Directory adds its children, a map from a name to a Node.
  • A line from Directory to Node has a filled diamond at Directory's end, labelled 1, and an arrow at Node's end, labelled 0..*: a directory is composed of zero or more child nodes, and each node belongs to exactly one directory.
  • An arrow labelled "find() asks" leads from FileSystem to Filter, whose operation matches(node, now) returns a boolean.
  • Two boxes point to Filter with hollow triangles: the primaries Name, Type, Size and Modified, whose value is a pattern, f or d, a size such as n or nc, or a number of days; and the combinators And, Or and Not, whose operands are one or two other Filters.

The design makes six choices, each with one main option:

  • Composite nodes: File and Directory share Node, so size and search treat a tree and a leaf alike.
  • Names in the parent: a directory maps names to children, so a rename changes one entry.
  • Parent links: each node knows its parent, so .. and the subtree check walk upwards.
  • One resolver: every operation resolves its path with the same walk, component by component.
  • Checks before changes: a move or a removal validates everything first, so a failure leaves the tree as it was.
  • Filters as objects: each search test is an object with matches(), combined by And, Or and Not.

The deep dives that decide whether this design holds:

  1. How does one resolver serve every operation, including those that create the last component?
  2. What does mv check, in which order, and what does a careless move do to the tree?
  3. Should a directory compute its size or cache it, and what does each cost?
  4. How do find’s tests become filter objects, with its rounding, its precedence and pruning?
  5. What changes when two threads rename at once?
  6. What do symbolic links add to resolution and to recursive calls?

An answer is judged on the requirements, the split between nodes, resolver and operations, the move rules, the size trade-off, the search design and the failure handling.

One resolver for every operation Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Moves: six checks, then two changes
    • Two careless moves, reproduced
  • A directory’s size: computed or cached, with numbers
  • find’s tests as filter objects
  • Two threads, and symbolic links later
  • Designs this one rejects
  • What each level is expected to cover
  • A 45-minute self-mock
  • Practise: symbolic links with loop detection
    • Symbolic links in the resolver, with loop detection
  • Check yourself
  • Interview questions
    • Why do file system designs model files and directories with the Composite pattern?
    • mv /projects /projects/archive/old must fail, while mv /projects/archive /old must work. How does your design tell the two apart, what does the check cost, and what would a move without it do to the tree?
    • du on the root of your in-memory file system takes 2 seconds with 5 million files, and a quota check needs the size of a user's folder on every write. Would you cache directory sizes? Give the costs in numbers and say how you keep the totals right.
    • Two threads run mv /a/x /b/x/y and mv /b/x /a/x/z at the same moment on your in-memory file system. Each move passes its subtree check. What can go wrong, and how do you lock so that it cannot?
  • Key takeaways

1 diagram · 4 runnable examples (drills.py, filters.py, fs.py, search.py, session.py, sizes.py) · 1 exercise with sample tests · 3 interview answers · about 29 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; nothing renews unless you choose to. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Low-level design: parking lot (the model)

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.