Command Design Pattern in Java: Undo, Redo and Queues

The Command pattern turns actions into objects. Build a Java playlist editor with working undo and redo, duplicate-song handling, and clear queueing limits.

Client supplies a Command to an invoker; the concrete command calls the receiver.

The Command pattern encapsulates a request as an instance with an operation that other code can invoke. The instance typically stores a receiver and the arguments needed to perform the request. A button, menu item or history manager can hold that object and run it later without knowing the methods that do the work. This is useful when an application needs to record actions, offer undo, or queue work.

Undo is possible when a command also defines how to reverse its effect; merely wrapping a request does not provide reversal automatically.

1. How the Command Pattern Works

The pattern separates the code that asks for an action from the object that performs it. Its participants have distinct jobs:

Participant Responsibility
Client Creates a concrete command and gives it the receiver and any arguments it needs.
Command Defines the operation the invoker can call. An undoable version also defines a reversal operation.
Concrete command Stores one request and calls the receiver to carry it out. It can keep information needed for undo.
Invoker Calls the command without knowing the receiver’s business methods. It may also keep a history.
Receiver Performs the actual work requested by the command.

The client supplies a command to the invoker. The invoker calls the command, which in turn calls the receiver. The invoker can therefore treat different requests the same way, even though each command performs different work.

The client supplies a command to an invoker, which calls the concrete command and its receiver

2. Command Pattern in Action

Our playlist editor lets a user add and remove songs. The editor also needs to reverse the latest edit and redo an edit that was just undone. A Playlist stores the songs, while a CommandHistory runs each action and remembers it. Every new action gets a fresh command object.

Pattern participant Playlist example Role in the edit
Client CommandDemo Creates an AddSong or RemoveSong request for a particular playlist and song.
Command Command Declares execute() and undo() for the history manager.
Concrete commands AddSong, RemoveSong Apply one edit and remember the position needed to reverse it.
Invoker CommandHistory Runs commands and moves completed actions between undo and redo stacks.
Receiver Playlist Adds or removes a song at a particular position.

A button or menu action can submit an edit through the history manager:

Playlist playlist = new Playlist();
CommandHistory history = new CommandHistory();
history.run(new AddSong(playlist, "Yesterday"));
history.undo();

The first call creates a request containing the target playlist and song title. The history manager executes that request and keeps it for a later undo. The second call removes the song that the request just added. The UI code needs the command and history operations; it does not perform the index bookkeeping itself.

For example, RemoveSong finds the first matching title and remembers its position before removing it. When the user chooses undo, the history manager asks that same command to put the title back at the saved position. This works even if the playlist contains the same title more than once.

CommandHistory runs AddSong and RemoveSong commands that edit Playlist

3. Command Pattern Implementation

The source uses JDK 17 or later. The small example keeps package-private supporting classes in CommandDemo.java. The excerpts below require imports from java.util for List, ArrayList, Deque, ArrayDeque and Objects. The repository includes those imports and the package declaration.

3.1. Defining the Request Contract and Receiver

The Command interface gives the history manager a common way to run and reverse an edit. The Playlist owns its list of songs. Its songs() method uses List.copyOf() to return an unmodifiable copy so callers cannot change that list outside the command history.

interface Command {
    void execute();
    void undo();
}

final class Playlist {
    private final List<String> songs = new ArrayList<>();

    int size() { return songs.size(); }
    void add(int index, String song) { songs.add(index, song); }
    int indexOf(String song) { return songs.indexOf(song); }
    String remove(int index) { return songs.remove(index); }
    List<String> songs() { return List.copyOf(songs); }
}

3.2. Capturing Enough Information to Undo

Each command stores its receiver and song title in its constructor. Execution also captures the position needed for reversal. AddSong saves the append position; RemoveSong saves the position of the first matching title.

final class AddSong implements Command {
    private final Playlist playlist;
    private final String song;
    private int addedAt;

    AddSong(Playlist playlist, String song) {
        this.playlist = Objects.requireNonNull(playlist);
        this.song = Objects.requireNonNull(song);
    }

    public void execute() {
        addedAt = playlist.size();
        playlist.add(addedAt, song);
    }

    public void undo() {
        playlist.remove(addedAt);
    }
}

final class RemoveSong implements Command {
    private final Playlist playlist;
    private final String song;
    private int removedAt;

    RemoveSong(Playlist playlist, String song) {
        this.playlist = Objects.requireNonNull(playlist);
        this.song = Objects.requireNonNull(song);
    }

    public void execute() {
        removedAt = playlist.indexOf(song);
        if (removedAt >= 0) {
            playlist.remove(removedAt);
        }
    }

    public void undo() {
        if (removedAt >= 0) {
            playlist.add(removedAt, song);
        }
    }
}

If two songs have the same title, removing by title during undo could remove the wrong occurrence. Saving the position makes the reversal precise. Later edits must be undone first so that the saved position still describes the original edit.

When the requested title is absent, indexOf() returns -1. This removal command changes nothing, and its undo() also changes nothing. The history still records the submitted action; a UI that should omit such actions needs an explicit changed/no-change result in the command contract.

3.3. Managing Undo and Redo History

CommandHistory has two stacks: one for actions that can be undone and another for actions that can be redone. A stack gives us the most recent action first. After a successful new action, run() adds it to the undo stack and clears the redo stack. Executing a new action after undo clears the redo history.

final class CommandHistory {
    private final Deque<Command> undo = new ArrayDeque<>();
    private final Deque<Command> redo = new ArrayDeque<>();

    void run(Command command) {
        Objects.requireNonNull(command).execute();
        undo.push(command);
        redo.clear();
    }

    boolean undo() {
        if (undo.isEmpty()) { return false; }
        Command command = undo.peek();
        command.undo();
        undo.pop();
        redo.push(command);
        return true;
    }

    boolean redo() {
        if (redo.isEmpty()) { return false; }
        Command command = redo.peek();
        command.execute();
        redo.pop();
        undo.push(command);
        return true;
    }
}

undo() and redo() inspect the top entry before calling it. They move that entry only after the call returns successfully. An exception therefore leaves the entry in its current stack. This does not reverse changes that a command made before throwing; such a command needs its own recovery policy.

This example assumes one thread and one history manager for all playlist edits. Changing the playlist directly would make saved positions unreliable. The same mutable command object is used again only for redo, not as a second independent history entry.

4. Demo

From the cloned repository root, compile and run the complete example:

javac -d target/command-demo -sourcepath src/main/java src/main/java/com/howtodoinjava/patterns/behavioral/command/CommandDemo.java
java -cp target/command-demo com.howtodoinjava.patterns.behavioral.command.CommandDemo

The program adds two copies of “Yesterday,” with “Hey Jude” between them. It then removes the first matching title and reverses that removal:

history.run(new AddSong(playlist, "Yesterday"));
history.run(new AddSong(playlist, "Hey Jude"));
history.run(new AddSong(playlist, "Yesterday"));
history.run(new RemoveSong(playlist, "Yesterday"));
history.undo();
history.redo();
history.undo();
history.run(new AddSong(playlist, "Help"));

These calls belong after the creation of a fresh playlist and history. The complete demo prints the playlist between operations. The final addition creates a new history branch after an undo:

After adding: [Yesterday, Hey Jude, Yesterday]
After removing first match: [Hey Jude, Yesterday]
After undo: [Yesterday, Hey Jude, Yesterday]
After redo: [Hey Jude, Yesterday]
After new action: [Yesterday, Hey Jude, Yesterday, Help]
Redo available: false

Undo restores the first “Yesterday” before “Hey Jude”; it does not move the later copy. After the new addition, redo() returns false because run() cleared the old redo branch.

5. Queued Work and Macro Commands

A command can be stored until another part of the application runs it. For in-process work, an ExecutorService accepts Runnable or Callable tasks. Its submit() method returns a Future; calling get() lets the caller observe a task failure. The playlist example runs synchronously and does not submit a job to an executor.

A macro command groups several commands so the user sees one action. Its execute() method can run them in order, and its undo() method can reverse them in the opposite order. If an operation fails partway through, the group is not automatically atomic. It needs a rule for partial work.

An in-memory command queue is not durable, and undo cannot automatically retract an external effect such as a sent email or payment. Durable jobs require stored request data and retry rules. External effects may need a separate compensating action.

6. Similar Patterns

These patterns may appear in the same editor, but they represent different things:

Pattern Deciding difference
Strategy Strategy selects how a task is performed; Command represents a particular request to perform work, often with a target and arguments.
Memento Memento stores a prior state; Command stores an action. A command can use a memento to restore state instead of manually reversing each change.

7. Frequently Asked Questions

Undo and deferred execution depend on more than the command interface. These cases clarify the requirements of this implementation.

7.1. Can every command be undone?

No. The basic pattern can have only an execution method. Undo works when the application defines a meaningful reversal and the command retains enough information to perform it. Some effects, such as an email already delivered, cannot be simply reversed.

7.2. What if a command fails during undo or redo?

The example leaves the command on its current stack if its method throws. The playlist may still have changed before that exception, so leaving the entry in place is not a complete recovery strategy. For commands that can fail partway through, define how to recover or report the partial result before allowing a retry.

7.3. Can I run the same command object twice?

This example creates a new object for each new user action and reuses an object only when redoing that action. AddSong and RemoveSong keep mutable position fields. Running the same object as two separate history entries would overwrite the information needed to undo the earlier entry.

8. Conclusion

Command makes a request an object that other code can invoke, store or queue. In the playlist editor, an undoable command also keeps the position needed to reverse its edit. The history manager then decides which action to undo or redo and clears redo when the user starts a new branch.

Happy Learning!

The complete example is available in the GitHub source folder.

Leave a Comment

  1. Hello Sir,
    I read your blog regarding command pattern, which is very informative.
    can you please share a link with me regarding simple way to learn spring security using different approaches like DAO etc.

    Looking forward

    Regards,
    Salman Khan

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.