The Command Pattern is a behavioral design pattern that encapsulates a request as an object, allowing you to parameterize methods with commands, queue them, or log their execution. In Java, this traditionally involves lots of boilerplate—but thanks to lambdas, we can simplify it dramatically.
In this tutorial, you'll learn how to implement the Command Pattern using Java lambdas, making your code concise, decoupled, and easy to maintain.
🎯 What Is the Command Pattern?
The Command Pattern decouples the object that invokes an operation from the one that knows how to perform it.
It consists of:
- Command interface: declares the execution method
- ConcreteCommand classes: implement the command interface
- Invoker: stores and executes the commands
- Receiver: performs the actual work
🧱 Traditional Command Pattern (Pre-Java 8)
interface Command {
void execute();
}
class Light {
void turnOn() { System.out.println("Light ON"); }
}
class TurnOnCommand implements Command {
private final Light light;
TurnOnCommand(Light light) { this.light = light; }
public void execute() {
light.turnOn();
}
}
class Switch {
void submit(Command command) {
command.execute();
}
}
Usage
Light light = new Light();
Command turnOn = new TurnOnCommand(light);
Switch s = new Switch();
s.submit(turnOn);
✅ Functional Command Pattern with Lambdas
We can replace the Command
interface with Runnable
, Consumer<T>
, or a custom @FunctionalInterface
.
Simplified Example Using Runnable
Light light = new Light();
Runnable turnOn = () -> light.turnOn();
Switch s = new Switch();
s.submit(turnOn);
Switch Class
class Switch {
void submit(Runnable command) {
command.run();
}
}
No concrete command class needed!
🔧 Using Custom Functional Interface
@FunctionalInterface
interface Command {
void execute();
}
Now use any lambda that matches this shape:
Command shutdown = () -> System.out.println("Shutting down...");
shutdown.execute();
🛠️ More Powerful Commands with Parameters
Use Consumer<T>
or BiConsumer<T, U>
for commands with parameters.
BiConsumer<String, Integer> printNTimes = (msg, n) -> {
for (int i = 0; i < n; i++) System.out.println(msg);
};
printNTimes.accept("Hello", 3);
🔄 Command Queue Example
Queue<Runnable> commandQueue = new LinkedList<>();
commandQueue.add(() -> System.out.println("Task 1"));
commandQueue.add(() -> System.out.println("Task 2"));
while (!commandQueue.isEmpty()) {
commandQueue.poll().run();
}
🧠 Real-World Analogy: Remote Control
Each button on a remote control can trigger a different command, which could be represented as a lambda:
Map<String, Runnable> remote = new HashMap<>();
remote.put("power", () -> System.out.println("TV ON"));
remote.put("volumeUp", () -> System.out.println("Volume +"));
remote.get("power").run();
remote.get("volumeUp").run();
🧩 Integration with Streams
List<Runnable> commands = List.of(
() -> System.out.println("Start"),
() -> System.out.println("Process"),
() -> System.out.println("End")
);
commands.forEach(Runnable::run);
📌 What's New in Java?
Java 8
- Lambdas,
Runnable
as command,java.util.function
for parameterized actions
Java 9
- Flow API (useful for reactive command pipelines)
Java 11
var
in lambda parameters
Java 17
- Sealed interfaces allow safer command modeling
Java 21
- Virtual threads + lambdas = lightweight task scheduling with command semantics
✅ Conclusion and Key Takeaways
- Lambdas simplify the Command Pattern by eliminating boilerplate classes.
- Use
Runnable
,Consumer<T>
, or custom functional interfaces for command actions. - Commands can be stored, composed, or passed just like data.
- Perfect for task queues, UI event handlers, job schedulers, and more.
❓ Expert FAQ
Q1: Can I use checked exceptions in lambdas for command pattern?
Not directly—use a wrapper or define your own functional interface that allows throwing.
Q2: What’s the benefit of using Runnable
over a custom interface?
Convenience—Runnable
is standard, but custom interfaces allow parameters and more expressiveness.
Q3: Can I undo a command?
Yes—store an undo lambda along with the command, or use a reversible command model.
Q4: Are commands garbage collected like any object?
Yes. If no references remain, the lambda and captured objects are GC-eligible.
Q5: Can lambdas be serialized as commands?
Technically yes, but fragile—prefer explicit classes if serialization is required.
Q6: What’s the difference between Command
and Strategy
patterns with lambdas?
Command represents action execution, Strategy represents algorithm selection.
Q7: Can I use this in Spring or frameworks?
Absolutely. Use it for task runners, event handlers, or custom logic injection.
Q8: Can I schedule lambdas using ExecutorService
?
Yes—executor.submit(() -> doSomething());
is a classic use of the functional command pattern.
Q9: Are functional commands thread-safe?
Only if they don’t capture mutable shared state or handle it correctly.
Q10: Is this pattern useful in microservices?
Yes—great for background jobs, async handlers, or CQRS command processing pipelines.