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:
- store the action for later
- run it at a specific time
- undo it when needed
- log it for auditing or retries
- 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?
How is Command different from Strategy?
Why do undo/redo systems love commands?
When would you choose a command queue over direct calls?
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:
- Java 21 implementation
- Kotlin implementation
- Scala 2 implementation
- Java 21 tests
- Kotlin tests
- Scala 2 tests
- Scala 3 tests
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.