LLD Dojo

Command

Core. Expect to meet this one, and expect to be asked for it by name.

Start with the problem

An in-memory file system needs an undo for its last destructive operation, starting with delete. The obvious way to support one undo is to remember what delete removed, and put it back on request.

void delete(String path) {
    Node removed = tree.get(path);
    lastRemoved = new Undo(path, removed);   // remembered for one undo, and only one
    tree.unbind(path);
}

void undo() {
    if (lastRemoved != null) {
        tree.bind(lastRemoved.path(), lastRemoved.node());
        lastRemoved = null;
    }
}

For a single kind of operation and a single level of undo, this is fine, and it says exactly what it does.

Watch where it goes

A second operation needs undo too: mv. Undoing a move means moving the node back, which is a different fix-up than rebinding a deleted node, so undo grows a branch, and lastRemoved grows a sibling field for the last move. A third operation, write, needs undo as well, and it has to remember the file's previous content, not a node to rebind at all.

The real cost shows up once more than one undo is asked for. lastRemoved only remembers the single most recent thing, of whichever kind. A second requirement, undo twice in a row, needs somewhere to put a second remembered action. Without a shared shape, every field above turns into a list of its own kind, and each list has to be checked in the right order against the others.

The move

Give every undoable operation the same shape: something that knows how to redo itself and how to undo itself, held in one ordinary list in the order it happened.

public interface FileOp {
    void redo();
    void undo();
}

public final class DeleteOp implements FileOp {
    private final InMemoryFileSystem fs;
    private final String path;
    private Node removed;

    public DeleteOp(InMemoryFileSystem fs, String path) {
        this.fs = fs;
        this.path = path;
    }

    @Override public void redo() {
        removed = fs.remove(path);
    }

    @Override public void undo() {
        fs.restore(path, removed);
    }
}

A Deque<FileOp> replaces lastRemoved and its would-be siblings. Every operation, whatever kind it is, pushes itself onto the same deque after redo() runs. Undo pops one off and calls undo() on it, without needing to know whether it undoes a delete, a move, or a write. A fourth operation that needs undo is a fourth class implementing FileOp, and neither undo() nor the deque changes to make room for it.

What modern Java changes here

FileOp has two methods here because undo needs its own state, removed, captured at the moment the operation ran. Where an action has nothing to undo, the whole pattern often collapses to a single method. Runnable, or a method reference, already is that shape. A job queue that only ever needs to run work later has no reason to define its own one-method interface, when Runnable::run already says the same thing. Command in the original sense is close to a Runnable with execute() renamed, once undo, logging, or queuing before execution are not part of the requirement.

A command that must run later, on a different thread, is a Callable<T> if it needs to return a value or throw a checked exception, and a plain Runnable if it does not. Either shape already composes with ExecutorService, so the invoker half of this pattern is often nothing to write at all: it is a thread pool already in the standard library.

When naming it is wrong

A single operation, called once, right where it is decided on, does not need to be wrapped as an object. tree.unbind(path) called directly, inside delete, is a method call doing what it says. Turning it into a DeleteOp before undo, logging, or queuing is actually needed buys an interface, a class, and a level of indirection. The call site never needed to be more than one line.

The threshold: reach for this once an operation has to be undone, retried, logged before it runs, or queued for later, separately from the code that decides to run it. A method call with one caller and no such requirement in sight is over-engineered (premature interface) under the Standard's D3 dimension, the same as any other seam built before a second reason for it exists.

Where this lives in the app

No syllabus item in this app currently builds this pattern out. Flagging this for whoever owns _index.json: _index.json anchors Command to B4, but B4 is corpus/logger, built entirely around Observer, and nothing there encapsulates a request as an object. The undo example above is invented, kept in the file-system domain so it sits next to composite.md and iterator.md rather than introducing a new one.

All reference pages