Command Pattern: Making Requests into Objects

java java21 scala scala2 scala3 kotlin design-patterns behavioral-patterns command-pattern

Imagine you’ve just typed a dozen lines in a text editor and then hit undo three times. The message that disappears is not magic. It is a clean example of the Command pattern at work: each action is wrapped as an object that knows how to execute itself and how to reverse itself.

The Problem: A method call is not enough

Most code starts with a direct call:

  • editor.insert("Hello")
  • paymentService.chargeCard(...)
  • queue.publish("job-42")

That works until the caller needs more than one behavior. We want to:

  1. store the action for later
  2. run it at a specific time
  3. undo it when needed
  4. log it for auditing or retries
  5. support macro recording

If those requirements appear, the logic starts leaking across the application. You end up with flags, special cases, and a pile of hidden coupling. The Command pattern turns the action itself into an object so the system can treat it like data.

Key Concepts

Concept In this example Why it matters
Command InsertTextCommand Encapsulates the action and its undo logic
Receiver DocumentEditor Owns the real state that changes
Invoker CommandHistory Executes, queues, and tracks commands
History undo/redo stacks Makes reversible workflows possible
Transactional behavior command execution + lifecycle Keeps the call site simple and consistent

Real Use Case: Undoable Editor Actions

A text editor is one of the clearest places to see the pattern. Every user action is a command:

  • insert text
  • delete text
  • format selection
  • paste block
  • replace word

Each command can be recorded in history, undone, redone, or replayed. Without commands, undo becomes a big list of if statements and manual state resets.

The pattern is also useful in:

  • payment workflows with retry and rollback
  • job queues with scheduled execution
  • macro recorders
  • background processing pipelines

The Solution: Commands Across JVM Languages

This example wraps insert operations in a command object and keeps an undo/redo stack in a CommandHistory class.

public final class DocumentEditor {
    private final StringBuilder content = new StringBuilder();

    public void insert(int index, String value) {
        content.insert(index, value);
    }

    public void delete(int start, int end) {
        content.delete(start, end);
    }

    public String text() {
        return content.toString();
    }
}

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

final class InsertTextCommand implements Command {
    private final DocumentEditor editor;
    private final int index;
    private final String value;

    InsertTextCommand(DocumentEditor editor, int index, String value) {
        this.editor = editor;
        this.index = index;
        this.value = value;
    }

    public void execute() {
        editor.insert(index, value);
    }

    public void undo() {
        editor.delete(index, index + value.length());
    }
}
class DocumentEditor {
    private val content = StringBuilder()

    fun insert(index: Int, value: String) {
        content.insert(index, value)
    }

    fun delete(start: Int, end: Int) {
        content.delete(start, end)
    }

    fun text(): String = content.toString()
}

interface Command {
    fun execute()
    fun undo()
}

class InsertTextCommand(
    private val editor: DocumentEditor,
    private val index: Int,
    private val value: String,
) : Command {
    override fun execute() {
        editor.insert(index, value)
    }

    override fun undo() {
        editor.delete(index, index + value.length)
    }
}
final class DocumentEditor {
  private val content = new StringBuilder

  def insert(index: Int, value: String): Unit = content.insert(index, value)
  def delete(start: Int, end: Int): Unit = content.delete(start, end)
  def text: String = content.toString
}

trait Command {
  def execute(): Unit
  def undo(): Unit
}

final case class InsertTextCommand(editor: DocumentEditor, index: Int, value: String) extends Command {
  override def execute(): Unit = editor.insert(index, value)
  override def undo(): Unit = editor.delete(index, index + value.length)
}
final class DocumentEditor:
  private val content = StringBuilder()

  def insert(index: Int, value: String): Unit = content.insert(index, value)
  def delete(start: Int, end: Int): Unit = content.delete(start, end)
  def text: String = content.toString

trait Command:
  def execute(): Unit
  def undo(): Unit

final case class InsertTextCommand(editor: DocumentEditor, index: Int, value: String) extends Command:
  override def execute(): Unit = editor.insert(index, value)
  override def undo(): Unit = editor.delete(index, index + value.length)

Why this pattern is powerful

The important idea is not that the code is written as a class. The important idea is that the caller no longer knows the details of the work. It just says, “execute this command”.

That gives you some nice benefits:

  • you can log commands before execution
  • you can queue them for batch processing
  • you can reverse them in a clean undo stack
  • you can test each command in isolation
  • you can record a macro of commands and replay it later

The same structure shows up in real systems: a payment service may queue a ChargeCardCommand, a workflow engine may execute a DeployReleaseCommand, and a UI may store a CopyTextCommand for easy undo.

Comparison Table

Concern Java 21 Scala 2/3 Kotlin
Command contract interface + concrete implementations trait + case class interface + class
Undo logic explicit undo() method same same
History tracking Deque<Command> ArrayDeque / mutable stack ArrayDeque
Typical use UI editors, task queues domain workflows, replayable actions coroutine jobs, actions, app commands
Mental model object-oriented command object case class + function-like action data class + interface

Best Practices

  • Keep commands small and explicit. A command should represent one business action.
  • Put the state change in the receiver, not in the command invoker.
  • Store enough data to undo exactly what was done.
  • Think carefully about idempotency and retry semantics when commands are queued.
  • When you have command composition, prefer a clear history model over vague flags and booleans.

Interview Q&A: Command Pattern in Practice

What problem does the Command pattern solve?
It turns a request into an object so you can store it, delay it, inspect it, or undo it. That matters when the action is more than a direct method call, especially for editors, queues, and transactional flows. Instead of scattering logic across the UI and service layers, the command becomes the single unit of work.
How is Command different from Strategy?
Strategy chooses an algorithm. Command wraps an action. A strategy may tell you how to calculate a fee; a command tells you to "insert this text" or "charge this card" and records how to undo it. The key difference is intent: Strategy is about selecting behavior, while Command is about executing a request as a first-class object.
Why do undo/redo systems love commands?
Because each command knows both the forward action and the reverse action. When the user presses undo, the system simply asks the last command to reverse itself. That keeps history logic simple and avoids writing custom rollback logic everywhere the app mutates state.
When would you choose a command queue over direct calls?
Use a queue when the action must be delayed, retried, logged, or replayed. A payment system might queue a charge request, a worker service might process jobs from a queue, and a UI might record a series of edits for macro playback. Direct calls are simpler for one-off behavior; commands are better when the action has a lifecycle.

Conclusion

The Command pattern is a great fit when the code needs a clean lifecycle around an action: execute it, record it, undo it, replay it, or queue it. In Java, Scala, and Kotlin, the pattern takes different shapes, but the idea stays the same — treat each action as an object instead of as a loose pile of instructions.

That is exactly why it shows up in editors, workflows, and distributed processing systems. Once you see commands as first-class actions, the rest of the system becomes easier to test, easier to reason about, and much easier to undo when reality bites back.

Code Samples

All examples in this post are runnable and mirrored across the JVM languages in the repository:


This is part of our Design Patterns series for JVM developers. Start with Design Patterns in JVM Languages - Your Guide to the Top 10. Next related posts: Strategy Pattern: Choosing Algorithms at Runtime and Observer Pattern: Reacting to Changes.