Java design pattern deep dive
Command Pattern in Java: 100 interview questions with professional answers.
Learn how the Command pattern turns a request into a standalone object that can be stored, queued, logged, and undone; how it decouples an invoker from the receiver that does the real work; and how it shows up in the JDK as Runnable and Callable, in task queues, in message-driven command queues, and in CQRS-style command handlers.
What makes a good Command answer?
Interviewers want to see that you understand Command as a way to reify a request into an object, not just a callback with extra ceremony: correct role separation, honest treatment of state capture, and restraint about where logic belongs.
| Approach | Use when | Watch out for |
|---|---|---|
| Hand-written Command objects | You need real undo/redo, queuing, logging, or per-command metadata beyond a single action. | More boilerplate classes than a plain callback; resist adding business logic to the invoker instead. |
| Runnable / Callable | Fire-and-forget or result-producing work submitted to an ExecutorService, with no undo requirement. | No built-in undo/redo or metadata; Callable wraps checked exceptions in ExecutionException. |
| Method references / lambdas as commands | The action is stateless and simple enough that a functional interface reads more clearly than a class. | Retrofitting undo, serialization, or auditing later usually forces a refactor back into a class. |
| Persisted command queue (message broker) | Commands must survive a crash, be processed by another instance or service, or be retried safely. | Idempotency and ordering must be handled explicitly; duplicate delivery is the normal case, not an edge case. |
Topics
Interview questions and answers
Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.
1. Explain the Command design pattern in Java, name its four classic roles, and describe the real-world problem it solves by turning a request into a standalone object.
Command encapsulates a request as an object, so that a piece of behavior, along with the receiver it operates on and any arguments it needs, can be treated like any other piece of data: stored in a variable, passed to a method, placed in a queue, or logged to disk. The four classic roles are the Command interface declaring execute(), a ConcreteCommand that binds a specific Receiver and its arguments, an Invoker that holds a command and triggers it without knowing what it does, and a Client that constructs the concrete command and wires it to its receiver.
interface Command {
void execute();
}
class Light {
void turnOn() { System.out.println("Light on"); }
}
class TurnOnLightCommand implements Command {
private final Light light;
TurnOnLightCommand(Light light) { this.light = light; }
@Override public void execute() { light.turnOn(); }
}
class RemoteButton { // Invoker
private final Command command;
RemoteButton(Command command) { this.command = command; }
void press() { command.execute(); }
}
2. Design a Command interface for a Java application, including a discussion of whether execute() should return a value, throw checked exceptions, and whether undo() belongs on the same interface.
The minimal interface declares a single void execute() method with no checked exceptions, matching Runnable's shape so commands compose naturally with executors. If callers need a result, a parallel Command<R> with R execute() (mirroring Callable<V>) is cleaner than overloading one interface both ways. Whether undo() belongs on the base interface depends on whether every command in the system needs to be undoable; if only some do, put undo() on a separate UndoableCommand extends Command interface instead of forcing every implementation to support it.
interface Command {
void execute();
}
interface UndoableCommand extends Command {
void undo();
}
Keeping undo optional via a sub-interface avoids commands that can never sensibly be undone (like sending an email) being forced to implement a meaningless or exception-throwing undo().
3. Walk through implementing a ConcreteCommand class that binds a specific Receiver instance and its constructor arguments, and explain why the binding happens at construction time rather than at execute() time.
A ConcreteCommand stores a reference to its receiver and any parameters as final fields set in the constructor, so that by the time it is handed to an invoker, it is a fully self-contained unit of work requiring no further context to run. Binding at construction time, rather than passing the receiver into execute(), is what lets the same Command interface serve invokers that know nothing about receivers at all.
class TransferFundsCommand implements Command {
private final Account from;
private final Account to;
private final java.math.BigDecimal amount;
TransferFundsCommand(Account from, Account to, java.math.BigDecimal amount) {
this.from = from; this.to = to; this.amount = amount;
}
@Override
public void execute() {
from.withdraw(amount);
to.deposit(amount);
}
}
If the receiver were instead passed into execute(Receiver r), the invoker would need to supply it, reintroducing exactly the coupling Command is meant to remove.
4. Describe precisely what the Invoker in the Command pattern is responsible for, and list what it must never know about in order to keep the decoupling intact.
The invoker's entire job is to hold a reference to a Command and call execute() (and optionally undo()) on it at the appropriate moment, whether that moment is a button press, a scheduled tick, or a message arriving off a queue. It must never import, reference, or branch on the concrete command type, the receiver's type, or any business rule about what the command actually does; the moment an invoker contains a switch on command type, the decoupling has been lost.
class MenuItem { // Invoker
private Command command;
void setCommand(Command command) { this.command = command; }
void click() { command.execute(); } // no knowledge of what command does
}
5. Explain the role of the Receiver in the Command pattern, and clarify why the Receiver is often an existing domain class that was never designed with Command in mind.
The receiver is the object that actually knows how to perform the operation: a Light that knows how to turn on, an Account that knows how to withdraw and deposit, a Document that knows how to insert text. Command does not require the receiver to implement any special interface at all; it is frequently a plain domain class that predates the command layer entirely, and the ConcreteCommand's job is purely to invoke the right method on it with the right arguments.
This is precisely what makes Command lightweight to retrofit onto an existing codebase: you write a thin command wrapper around an existing method call rather than redesigning the domain class to know about commands, invokers, or undo.
6. Explain the Client's role in the Command pattern, and show, with code, how it constructs a ConcreteCommand and wires it to both a Receiver and an Invoker.
The client is the code that knows about all three other roles at once: it instantiates the receiver, constructs a concrete command wrapping that receiver, and hands the command to whichever invoker will trigger it later. This is usually application wiring code, such as a UI setup method or a Spring configuration class, not part of the ongoing runtime flow of invoker calling execute().
Light livingRoomLight = new Light(); // Receiver
Command turnOn = new TurnOnLightCommand(livingRoomLight); // ConcreteCommand
RemoteButton button = new RemoteButton(turnOn); // Invoker holds the command
button.press(); // later, invoker triggers it
Notice the invoker (RemoteButton) never touches Light directly; only the client, during wiring, connects the command to its receiver.
7. Implement the classic remote-control-and-light-switch example of the Command pattern in Java, and explain why this toy example is still the clearest way to introduce the pattern in an interview.
The remote control example maps every role onto something physically intuitive: the remote button is the invoker, the light is the receiver, the "turn on" command object sits between them, and swapping which command a button holds (turn on the light versus turn on the fan) demonstrates the decoupling without any distracting domain complexity.
interface Command { void execute(); }
class Fan {
void spin() { System.out.println("Fan spinning"); }
}
class SpinFanCommand implements Command {
private final Fan fan;
SpinFanCommand(Fan fan) { this.fan = fan; }
public void execute() { fan.spin(); }
}
class RemoteControl {
private Command slot;
void program(Command command) { this.slot = command; }
void pressButton() { slot.execute(); }
}
RemoteControl remote = new RemoteControl();
remote.program(new SpinFanCommand(new Fan()));
remote.pressButton(); // "Fan spinning" -- swap the command, same button, same invoker code
8. What concrete engineering benefit does decoupling the invoker from the receiver actually provide, beyond satisfying a design pattern checklist, and how would you justify introducing Command to a skeptical teammate?
The concrete benefit is that the invoker's code, and any code that only depends on the Command interface, never needs to change when new receivers or new operations are added; you write a new ConcreteCommand class and wire it in, without touching the invoker, the UI framework code, or the task-scheduling code. This directly enables features that are otherwise painful to retrofit: undo/redo, a queue of pending operations, an audit log of every action taken, and swapping what a keyboard shortcut or button does at runtime.
To a skeptical teammate, the strongest justification is naming the specific feature that needs it: "we need undo" or "we need to replay these later" is a much better argument than "it's more object-oriented," since Command genuinely adds a class per action and is not free.
9. Explain why treating a request as a first-class object, rather than as a direct method call, is the foundational idea of the Command pattern, and list the capabilities this unlocks that a direct call cannot provide.
A direct method call, light.turnOn(), exists only for the duration of the call on the stack; once it returns, there is no artifact left behind to inspect, store, repeat, or reverse. Wrapping that same call inside a Command object turns it into data with a lifetime independent of when it runs: it can sit in a list waiting to be executed later, be serialized and sent across a network or into a queue, be appended to a log for audit purposes, or be paired with its inverse for undo.
10. When is a plain, direct method call clearly preferable to introducing a full Command object, and what signals in a code review suggest Command is being used where a direct call would suffice?
If nothing downstream ever needs to queue, log, undo, replay, or swap the operation at runtime, a direct call is simpler, has no extra class to maintain, and is easier for a reader to trace. A signal that Command is overkill is a ConcreteCommand class used exactly once, constructed and executed in the same method, with no invoker actually holding onto it between construction and execution — at that point it is indistinguishable from, and strictly more verbose than, just calling the method.
// Overkill: constructed and executed immediately, nothing stored in between
new TurnOnLightCommand(light).execute();
// Simpler, and equally correct here:
light.turnOn();
11. What are the drawbacks of overusing the Command pattern across a codebase, and how does an excess of tiny one-method command classes hurt maintainability?
Every command is a new class (or lambda) that a reader must locate to understand what actually happens when it executes, adding a layer of indirection between "the button was pressed" and "the light turned on." When a codebase has dozens of one-line command classes that all funnel into the same invoker and none of them are ever queued, logged, or undone, the pattern has added ceremony without buying any of its real benefits.
12. Explain how to implement undo for a Command by storing an explicit inverse operation, and provide a Java example for a text-insertion command whose undo deletes exactly what was inserted.
Inverse-operation undo means the command itself knows how to reverse its own effect: an insert command's undo() deletes the same range it inserted, an increment command's undo() decrements by the same amount, and a "set field to X" command's undo() restores the previous value it recorded before overwriting it.
class InsertTextCommand implements UndoableCommand {
private final Document document;
private final int position;
private final String text;
InsertTextCommand(Document document, int position, String text) {
this.document = document; this.position = position; this.text = text;
}
@Override public void execute() { document.insert(position, text); }
@Override public void undo() { document.delete(position, position + text.length()); }
}
This approach is memory-efficient because it never stores a full document snapshot, only the small amount of state needed to reverse one specific change.
13. Explain how to implement undo for a Command using the Memento pattern, capturing a snapshot of the Receiver's state before execute() runs, and show when this approach is preferable to an inverse operation.
Instead of computing a logical inverse, the command captures a snapshot, a Memento, of the receiver's relevant state immediately before performing the operation, and undo() simply restores that snapshot wholesale. This is preferable when the operation's effect is too complex or too interdependent with other state to express cleanly as an inverse, such as a formatting operation that touches many attributes at once.
class ApplyFormattingCommand implements UndoableCommand {
private final Paragraph paragraph;
private final FormattingStyle newStyle;
private FormattingStyle previousStyle; // the memento
ApplyFormattingCommand(Paragraph paragraph, FormattingStyle newStyle) {
this.paragraph = paragraph; this.newStyle = newStyle;
}
@Override
public void execute() {
previousStyle = paragraph.getStyle(); // snapshot before mutating
paragraph.setStyle(newStyle);
}
@Override
public void undo() { paragraph.setStyle(previousStyle); }
}
14. Compare inverse-operation undo versus Memento-snapshot undo in terms of memory usage, correctness risk, and implementation effort, and explain how to decide between them for a given command.
| Aspect | Inverse operation | Memento snapshot |
|---|---|---|
| Memory usage | Small — only the delta needed to reverse the change. | Can be large — a full copy of whatever state might have changed. |
| Correctness risk | Higher — the inverse must be hand-derived and kept in sync with execute()'s logic. | Lower — restoring a snapshot is mechanical and cannot drift from execute()'s actual effect. |
| Implementation effort | Requires reasoning about the operation's true inverse, which isn't always obvious. | Requires only a correct copy/restore of state, which is usually straightforward. |
Prefer the inverse operation when the change is simple and well-understood (arithmetic, a single field, an insert/delete pair); prefer a Memento when the operation's effects are broad, interdependent, or hard to reverse logically, and the state being snapshotted is small enough that the memory cost is acceptable.
15. Design a command history mechanism supporting multi-level undo and redo using two stacks, and walk through exactly what happens to each stack when a new command executes, when undo is called, and when redo is called.
Maintain an undoStack of executed commands and a separate redoStack. Executing a new command pushes it onto undoStack and clears redoStack (see Q16 for why). Undo pops the top of undoStack, calls its undo(), and pushes it onto redoStack. Redo pops the top of redoStack, calls its execute() again, and pushes it back onto undoStack.
class CommandHistory {
private final Deque<UndoableCommand> undoStack = new ArrayDeque<>();
private final Deque<UndoableCommand> redoStack = new ArrayDeque<>();
void execute(UndoableCommand command) {
command.execute();
undoStack.push(command);
redoStack.clear();
}
void undo() {
if (undoStack.isEmpty()) return;
UndoableCommand command = undoStack.pop();
command.undo();
redoStack.push(command);
}
void redo() {
if (redoStack.isEmpty()) return;
UndoableCommand command = redoStack.pop();
command.execute();
undoStack.push(command);
}
}
16. Explain why the redo stack must be cleared whenever a new command executes after one or more undos, and describe the bug that results if this clearing step is forgotten.
Once a user undoes some commands and then performs a brand-new action, the undone commands' effects are no longer part of the document's actual history from this point forward — the timeline has branched. If the redo stack is not cleared, calling redo later would replay a command whose preconditions no longer hold (for example, re-inserting text at a position that has since shifted, or re-applying a discount to an order that has since changed), corrupting state or throwing an exception deep inside a stale command.
17. Design multi-level undo for a simple text editor, including how you would represent each keystroke or edit operation as a command and how the undo stack interacts with the editor's cursor position.
Each meaningful edit, an insertion, a deletion, or a paste, becomes an UndoableCommand capturing the affected range, the text involved, and, critically, the cursor position both before and after the edit, since undo should typically restore the cursor to where the user was before making the change, not just restore the text.
class DeleteRangeCommand implements UndoableCommand {
private final Document document;
private final int start, end;
private String deletedText;
private int cursorBefore;
DeleteRangeCommand(Document document, int start, int end) {
this.document = document; this.start = start; this.end = end;
}
@Override public void execute() {
cursorBefore = document.getCursor();
deletedText = document.delete(start, end);
}
@Override public void undo() {
document.insert(start, deletedText);
document.setCursor(cursorBefore);
}
}
18. Implement a MacroCommand (composite command) that groups several Command objects and executes them all as a single logical unit, and explain how a caller invokes it identically to a single command.
A macro command implements the same Command interface as its constituents, holds an ordered list of them, and its execute() simply iterates and executes each in sequence, so the invoker cannot tell whether it holds one command or a group of ten.
class MacroCommand implements UndoableCommand {
private final List<UndoableCommand> commands;
MacroCommand(List<UndoableCommand> commands) { this.commands = List.copyOf(commands); }
@Override
public void execute() {
for (UndoableCommand command : commands) command.execute();
}
@Override
public void undo() {
for (int i = commands.size() - 1; i >= 0; i--) commands.get(i).undo();
}
}
19. Explain why undoing a MacroCommand must reverse its constituent commands in the opposite order from which they were executed, and give an example where undoing in the original order would produce wrong results.
If command B's execution depended on an effect that command A produced, undoing A before B would leave B's undo() operating against state that no longer matches what it expects, since B's inverse was derived assuming A's effect was still present. Reversing order, undoing the most recent constituent first, mirrors exactly how you would manually back out of a sequence of dependent steps.
// A: withdraw $50 from checking. B: deposit $50 into savings (depends on A having succeeded).
// Correct undo order: undo B first (remove the deposit), then undo A (restore the withdrawal).
// Undoing A first would restore checking's balance while savings still shows the extra $50 -- inconsistent state.
20. Explain the structural relationship between the Command pattern's MacroCommand and the Composite pattern, and why a macro command is essentially Composite applied to Command.
Composite lets a client treat a single object and a collection of objects uniformly through a shared interface; MacroCommand is exactly that idea specialized to Command: it implements Command itself while internally holding a list of other Command objects, which may themselves be macro commands, forming a tree. An invoker calling execute() on the root cannot tell, and does not need to know, whether it is triggering one atomic action or an entire nested tree of actions.
21. Explain how a single Command instance can be shared across a menu item, a toolbar button, and a keyboard shortcut in a desktop Java application, so that all three trigger identical behavior and reflect identical enabled/disabled state.
Construct one Command instance representing the action (for example, "Save Document"), and register that same instance with the menu item's click handler, the toolbar button's click handler, and the keyboard shortcut's key binding. Because all three invokers hold a reference to the exact same command object, disabling that command once (say, because the document has no unsaved changes) automatically disables all three UI entry points without needing to synchronize three separate flags.
Command save = new SaveDocumentCommand(document);
menuItem.setCommand(save);
toolbarButton.setCommand(save);
keyBinding.bind("ctrl S", save); // same instance, three triggers
22. Explain how javax.swing.Action packages the Command pattern together with a label, icon, and enabled state, and show how one Action instance is attached to both a JMenuItem and a JButton.
Action extends ActionListener (the invoker-facing execute contract) and adds properties for a display name, a small icon, a mnemonic, a tooltip, and an enabled flag, all bundled into one object. Because Swing components query these properties, attaching the same Action to a JMenuItem and a JButton keeps their label, icon, and enabled state synchronized automatically whenever the action's properties change.
Action saveAction = new AbstractAction("Save", saveIcon) {
@Override public void actionPerformed(ActionEvent e) { document.save(); }
};
saveAction.setEnabled(document.isDirty());
JMenuItem saveMenuItem = new JMenuItem(saveAction);
JButton saveButton = new JButton(saveAction); // same Action, synchronized enabled/label/icon
23. Discuss how to add a canExecute() (or isEnabled()) guard to a Command so that an invoker can decide whether to gray out a UI control before the user even attempts to trigger it.
Add a side-effect-free query method, such as boolean canExecute(), that the invoker calls whenever the relevant state might have changed (on focus, on selection change, or on a periodic UI refresh) to decide whether to enable or disable the associated control, entirely separate from the act of actually running the command.
class DeleteSelectionCommand implements Command {
private final Selection selection;
DeleteSelectionCommand(Selection selection) { this.selection = selection; }
boolean canExecute() { return !selection.isEmpty(); }
@Override
public void execute() {
if (!canExecute()) throw new IllegalStateException("Nothing selected");
selection.deleteAll();
}
}
Checking canExecute() both when refreshing the UI and defensively at the top of execute() guards against a race where state changed between the check and the click.
24. Explain how java.lang.Runnable serves as a lightweight, ad hoc realization of the Command pattern in the JDK, and identify which of the four classic Command roles it corresponds to.
Runnable's single void run() method is structurally identical to a Command interface with only execute(); a lambda or anonymous class implementing Runnable plays the ConcreteCommand role, closing over whatever receiver and arguments it needs, while anything that calls run(), a Thread, an ExecutorService, or a Timer, plays the invoker role without knowing anything about what the runnable actually does.
Runnable turnOnLight = () -> light.turnOn(); // ConcreteCommand, closure-based
new Thread(turnOnLight).start(); // Invoker: doesn't know what the Runnable does
25. Explain how java.util.concurrent.Callable<V> extends the Runnable-as-Command idea to support a result value and checked exceptions, and show how it differs structurally from Runnable.
Callable<V> declares V call() throws Exception, letting the command both return a result to whoever eventually inspects it and propagate a checked exception, neither of which Runnable's void run() supports. This makes Callable a better fit for commands representing a computation whose outcome matters, such as "fetch this record" or "compute this report", submitted to an executor and retrieved later via a Future<V>.
Callable<Order> fetchOrder = () -> orderDao.findById(orderId); // may throw checked SQLException
Future<Order> future = executorService.submit(fetchOrder);
Order order = future.get(); // result retrieved once the command has run
26. Explain how ExecutorService.submit() acts as an Invoker for Runnable and Callable commands, dispatching them to a pool of worker threads without knowing what each command does.
ExecutorService is a general-purpose invoker: it accepts anything shaped like a command (Runnable or Callable<V>), places it on an internal work queue, and hands it to whichever worker thread becomes free, with zero knowledge of the receiver or business logic the command wraps. This is Command's invoker/receiver decoupling at industrial scale: the same thread pool can execute wildly different kinds of work submitted from unrelated parts of an application.
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> emailService.send(message)); // one kind of command
pool.submit(() -> reportGenerator.generate(reportId)); // an unrelated kind of command
pool.shutdown();
27. Compare Runnable and Callable against a full hand-written GoF Command implementation, and list specifically what capability is missing from the JDK's lightweight functional versions.
| Capability | Runnable / Callable | Hand-written Command |
|---|---|---|
| Undo | Not supported — no undo() method exists on either interface. | Supported via a companion undo() method or a sub-interface. |
| Metadata (name, description, audit fields) | None — a lambda has no identity beyond its captured state. | Can carry arbitrary fields for logging, display, or replay. |
| canExecute() precondition | Not standardized — must be checked separately before submission. | Can be a first-class method on the command itself. |
| Serialization for a persisted queue | Lambdas are notoriously awkward to serialize reliably. | A plain class with serializable fields is straightforward to persist. |
Use Runnable/Callable for simple, fire-and-forget or result-producing work; reach for a hand-written Command class the moment you need any of the rows above.
28. Design a task queue using the Command pattern where worker threads pull Command objects off a shared BlockingQueue and execute them, and explain why this decouples task producers from task consumers.
Producers construct Command objects representing units of work and push them onto a BlockingQueue<Command>; a pool of worker threads independently pulls commands off the queue and calls execute(), with neither side aware of the other's identity, pace, or implementation. This is essentially reimplementing (a simplified version of) what ThreadPoolExecutor already does internally.
BlockingQueue<Command> queue = new LinkedBlockingQueue<>();
Runnable worker = () -> {
while (true) {
try {
Command command = queue.take(); // blocks until work arrives
command.execute();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
};
29. Explain the transactional outbox pattern and how it uses a Command-shaped record to guarantee that a command is never lost between being decided upon and being published to a message broker.
Instead of writing to the database and publishing a message in two separate, non-atomic steps, the outbox pattern writes the command (as a serialized row) into an "outbox" table in the same local transaction as the business state change, then a separate poller or change-data-capture process reads unpublished outbox rows and publishes them to the broker, marking them as sent. This guarantees the command is durably recorded before anyone tries to act on it, even if the process crashes between the database commit and the publish.
@Transactional
void placeOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
outboxRepository.save(new OutboxRecord("OrderPlacedCommand", toJson(order))); // same transaction
}
30. Design a Kafka-based persisted command queue where each Kafka message represents a serialized Command, and describe how a consumer deserializes and executes it against the appropriate Receiver.
Producers serialize a command (typically as JSON or Avro, with a type discriminator field) and publish it to a Kafka topic keyed by the entity it targets, so commands for the same entity land on the same partition and are processed in order. A consumer reads each record, resolves the command type to a handler, deserializes the payload into that handler's expected shape, and invokes the receiver.
class TransferFundsCommandHandler implements CommandHandler<TransferFundsCommand> {
private final AccountRepository accounts;
TransferFundsCommandHandler(AccountRepository accounts) { this.accounts = accounts; }
@Override
public void handle(TransferFundsCommand command) {
Account from = accounts.findById(command.fromAccountId());
Account to = accounts.findById(command.toAccountId());
from.withdraw(command.amount());
to.deposit(command.amount());
accounts.saveAll(from, to);
}
}
31. Design a RabbitMQ-based persisted command queue and explain how RabbitMQ's per-message acknowledgment model changes the failure-handling story compared to Kafka's offset-based consumption for command processing.
A producer publishes a serialized command to a durable queue; a consumer receives it, executes the corresponding handler, and only then sends a manual acknowledgment, so an unacknowledged message (due to a crash or exception) is automatically redelivered, either to the same consumer or another one, without any offset bookkeeping the application must manage itself.
channel.basicConsume(queueName, false, (consumerTag, delivery) -> {
try {
Command command = deserialize(delivery.getBody());
command.execute();
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
} catch (Exception ex) {
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true); // requeue for retry
}
}, consumerTag -> {});
Unlike Kafka, where redelivery means re-reading from an earlier offset (potentially replaying many messages), RabbitMQ's per-message nack targets exactly the failed message, but that also means message ordering guarantees are weaker once retries and multiple consumers are involved.
32. Explain the idempotency concerns that arise when a queued Command is redelivered and executed more than once due to a consumer crash after execute() but before acknowledgment, and how you would guard against double execution.
If a consumer executes a command, such as "charge this card," but crashes before acknowledging the message, the broker will redeliver it, and a naive handler will execute the charge a second time. The command's receiver-side operation must therefore be made idempotent, or the handler must track which commands have already been applied, so redelivery is safe rather than destructive.
33. Design an idempotency key strategy for commands processed from a queue, including where the key is generated, how it is checked, and what storage backs the deduplication check.
The producer generates a unique idempotency key per logical command, typically a UUID assigned at the moment the command is first created, and includes it as a field on the serialized command. The consumer, before executing, checks a deduplication store (a database table or a fast key-value store like Redis) for that key; if already present, it skips execution and returns the previously recorded result, otherwise it executes and records the key atomically with the state change.
@Transactional
void handle(ChargeCardCommand command) {
if (processedCommands.exists(command.idempotencyKey())) return; // already applied
paymentGateway.charge(command.accountId(), command.amount());
processedCommands.record(command.idempotencyKey()); // same transaction as the effect, where possible
}
34. Explain what "at-least-once delivery" means for a message-driven command queue, and why designing for occasional duplicate execution is usually more practical than trying to guarantee exactly-once delivery at the transport layer.
At-least-once delivery means the messaging system guarantees a command will be delivered one or more times, never zero, trading the possibility of duplicates for the certainty that nothing is silently lost. True exactly-once delivery across a network is famously difficult (it requires coordinating the broker, the consumer's side effects, and the acknowledgment atomically), so most production systems accept at-least-once transport and instead make the command handler idempotent, which is a strictly easier and more portable engineering problem.
35. Design a retry strategy with exponential backoff for a Command that fails during execution against a flaky downstream Receiver, and explain how many attempts and what backoff parameters you would choose and why.
Wrap the command's execution in a retry loop that catches transient failures, waits an exponentially increasing delay (often with jitter to avoid synchronized retry storms across many consumers), and gives up after a bounded number of attempts, routing the command to a dead-letter destination rather than retrying forever.
void executeWithRetry(Command command, int maxAttempts) {
long delayMs = 200;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
command.execute();
return;
} catch (TransientException ex) {
if (attempt == maxAttempts) throw new CommandFailedException(command, ex);
sleep(delayMs + ThreadLocalRandom.current().nextLong(0, delayMs));
delayMs *= 2;
}
}
}
Only retry exceptions you have classified as transient (timeouts, connection resets); retrying a validation failure or a business rule rejection wastes attempts on an error that will never succeed.
36. Explain the role of a dead-letter queue for commands that repeatedly fail execution, and describe what operational tooling you would build around it so failed commands are not simply forgotten.
A dead-letter queue (DLQ) is where a command lands after exhausting its retry budget, preserving the exact failed command (and ideally the failure reason and attempt count) instead of discarding it, so an operator can investigate and decide whether to fix the underlying issue and replay the command, or discard it as invalid.
Useful tooling includes an alert when the DLQ depth crosses a threshold, a dashboard showing failure reasons grouped by command type, and a safe "replay from DLQ" tool that re-enqueues a command for retry only after a human or an automated check confirms the root cause is resolved.
37. Compare validating a Command's preconditions before it is ever constructed or enqueued (fail-fast) versus validating inside execute() at the moment it actually runs, and explain the risk each approach carries.
Fail-fast validation, checked by the client before the command is even created or enqueued, gives immediate feedback and avoids wasting queue capacity or worker time on doomed work, but it risks becoming stale if conditions change between validation and eventual execution (a classic time-of-check-to-time-of-use gap), especially for commands that sit in a queue for a while. Validating again inside execute(), immediately before performing the receiver-side action, catches that staleness but means invalid commands are only detected once they reach the front of the queue.
The safest designs do both: reject obviously invalid commands immediately for fast feedback, and re-check the preconditions that could plausibly have changed by the time execute() actually runs.
38. Discuss where validation logic should live in a Command-based design: on the Command itself, on the Invoker, or on the Receiver, and justify your answer.
Validation that only concerns the shape of the command's own data (a null check, a range check on an amount) belongs on the command, since it is intrinsic to that request regardless of receiver. Validation that depends on the receiver's current state (sufficient balance, whether an order is still cancellable) belongs on the receiver, since only the receiver has authoritative, up-to-date state to check against. The invoker should contain no validation at all, since its entire purpose is to be agnostic to what the command represents.
39. Explain the difference between the Command pattern and the Strategy pattern, since both wrap a piece of behavior behind a single-method interface, and clarify when each is the correct choice.
Both patterns look structurally similar, an interface with one abstract method implemented by several classes, but their intent differs: Strategy encapsulates an interchangeable algorithm for accomplishing the same overall task (different ways to sort, different ways to calculate a discount), typically invoked synchronously and with no expectation of undo, storage, or queuing. Command encapsulates a request to do something, most valuable precisely because it can be decoupled from when it runs, stored, logged, queued, or undone.
// Strategy: interchangeable algorithm for the same operation
interface DiscountStrategy { BigDecimal apply(BigDecimal price); }
// Command: a request to be performed, potentially undoable, queueable
interface Command { void execute(); }
Ask "do I need to swap the algorithm used right now?" for Strategy, versus "do I need to decouple deciding what to do from when/where it actually happens?" for Command.
40. Explain the difference between the Command pattern and the Chain of Responsibility pattern, given both can involve a request traveling through application code, and clarify the structural distinction.
Command binds a request to exactly one known receiver at construction time; there is no searching for who should handle it. Chain of Responsibility does the opposite: a request travels along a chain of candidate handlers, each deciding whether to handle it or pass it further down the chain, with the sender not knowing in advance which handler, if any, will ultimately process it.
The two patterns compose well together: a command bus can use a chain of middleware handlers (logging, validation, authorization) that each inspect a command before it reaches its single, definitively bound receiver at the end of the chain.
41. Explain the difference between the Command pattern and the Observer pattern, and describe a scenario where the two are naturally combined.
Command is about triggering one specific, known action against one specific receiver on demand; Observer is about broadcasting a notification of something that already happened to an open-ended, potentially empty set of subscribers who were never known in advance to the subject. They combine naturally when, after a command executes, its receiver publishes an event that unrelated observers react to, for example a PlaceOrderCommand executing and the Order receiver then notifying an inventory-reservation observer and an email-notification observer, neither of which the command itself knows about.
42. Explain the difference between the Command pattern and the Template Method pattern, since both can structure "do this operation" logic, and clarify why they solve different problems.
Template Method fixes an algorithm's overall skeleton in a base class and lets subclasses override specific steps, varying behavior through inheritance and a single call chain resolved at compile time for a given subclass. Command instead varies behavior through composition and runtime substitution of an entire object implementing a one-method interface, with no shared skeleton or fixed sequence of steps at all — a command's execute() can do anything, unconstrained by any template.
43. Explain precisely how the Command pattern and the Memento pattern collaborate to implement undo, and clarify which pattern is responsible for what part of that collaboration.
Command is responsible for knowing when and how to reverse an action, exposing an undo() entry point that the history stack calls; Memento is responsible for how a piece of state is captured and restored without violating encapsulation, typically by having the receiver produce an opaque snapshot object that only it knows how to interpret. A memento-based undoable command holds a memento internally and, in undo(), hands that memento back to the receiver to restore, rather than reaching into the receiver's fields directly.
class Memento { private final Object state; Memento(Object state) { this.state = state; } }
class ApplyDiscountCommand implements UndoableCommand {
private final Cart cart;
private Memento snapshot;
ApplyDiscountCommand(Cart cart) { this.cart = cart; }
public void execute() { snapshot = cart.createMemento(); cart.applyDiscount(); }
public void undo() { cart.restore(snapshot); }
}
44. Discuss how to write a unit test for a ConcreteCommand class in isolation, mocking the Receiver and asserting that execute() invokes exactly the correct receiver method with the correct arguments.
Since a command's job is to translate its own state into one or more calls on its receiver, tests should mock the receiver, invoke execute() on the command, and verify the receiver was called with the exact arguments the command was constructed with, then separately test undo() restores the expected prior state if the command is undoable.
@Test
void executeWithdrawsThenDeposits() {
Account from = mock(Account.class);
Account to = mock(Account.class);
TransferFundsCommand command = new TransferFundsCommand(from, to, new BigDecimal("50.00"));
command.execute();
verify(from).withdraw(new BigDecimal("50.00"));
verify(to).deposit(new BigDecimal("50.00"));
}
45. Explain how to test Invoker logic, such as a command history's execute/undo/redo bookkeeping, independently of what any particular concrete command actually does.
Test the invoker (for example, CommandHistory from Q15) against a trivial fake or mocked UndoableCommand whose only job is to record how many times execute() and undo() were called, so the test verifies the invoker's stack bookkeeping and ordering, not any receiver-specific behavior.
@Test
void redoReplaysCommandAfterUndo() {
UndoableCommand command = mock(UndoableCommand.class);
CommandHistory history = new CommandHistory();
history.execute(command);
history.undo();
history.redo();
verify(command, times(2)).execute(); // once initially, once via redo
verify(command, times(1)).undo();
}
46. Discuss the thread-safety concerns of a Command queue shared by multiple producer threads and multiple consumer worker threads, and how a properly designed BlockingQueue-based invoker avoids race conditions.
A correctly chosen concurrent queue implementation, such as LinkedBlockingQueue or ArrayBlockingQueue, already handles safe concurrent insertion and removal internally, so the queue itself is not the risk; the risk instead comes from the commands' own state, if two consumers can somehow obtain and execute the same command instance concurrently, or if a command's execute() mutates shared receiver state without synchronization of its own.
47. Explain how to use the Command pattern to build an audit log recording every executed command in a system for compliance purposes, and describe what fields such an audit record should capture.
Because a command object already carries everything needed to describe "what happened," a decorator or wrapping invoker can record each one, before or after execution, as an immutable audit entry: the command's type, its serialized arguments, the identity of who or what triggered it, a timestamp, and the outcome (success, failure, and any relevant error). This produces a compliance-grade trail without scattering logging calls across every receiver method.
class AuditingCommandDecorator implements Command {
private final Command delegate;
private final AuditLog auditLog;
private final String actor;
AuditingCommandDecorator(Command delegate, AuditLog auditLog, String actor) {
this.delegate = delegate; this.auditLog = auditLog; this.actor = actor;
}
@Override
public void execute() {
auditLog.record(actor, delegate.getClass().getSimpleName(), Instant.now());
delegate.execute();
}
}
48. Explain the conceptual relationship between the Command pattern and event sourcing: how a stream of executed commands, or the events they produce, can serve as the definitive audit trail and reconstruction mechanism for an entity's state.
In an event-sourced system, an entity's current state is derived by replaying every event it has ever produced, and each of those events is typically the direct result of validating and executing a command against the entity. The command represents intent ("transfer $50"), while the resulting event represents the fact that it happened ("$50 was transferred"); storing the append-only stream of events (rather than just the current state) gives you both a full audit trail and the ability to rebuild state at any point in history.
49. Discuss whether the "Command" object in CQRS (Command Query Responsibility Segregation) is the same thing as the GoF Command design pattern, and explain the nuance an interviewer is really testing with this question.
They are related in spirit but not identical: CQRS's "command" is an intent-expressing message (a plain data object like TransferFundsCommand{fromId, toId, amount}) sent to a command handler that validates it against an aggregate and produces events; it is a messaging and architectural convention, not necessarily an object implementing an execute() method with a bound receiver. The GoF Command pattern, by contrast, is specifically about the object owning its own execution logic and optionally its own undo, bound directly to a receiver at construction time.
In practice, a CQRS command is closer to a plain request DTO routed by an external command bus/handler than to a self-executing GoF ConcreteCommand, though many CQRS frameworks borrow GoF Command's vocabulary and some of its intent (treating a request as a first-class, loggable, queueable object). A strong answer names both the overlap (reifying intent as an object) and the difference (who owns execute() and the receiver binding).
50. Design a command bus that routes an incoming Command object to the correct registered CommandHandler based on its runtime type, and explain why this avoids a large if/else or switch chain in application code.
A command bus maintains a registry mapping each command class to its handler, and its single dispatch(Command) method looks up the handler for the command's runtime class and delegates to it, so adding a new command type only means registering a new handler, never touching the bus's dispatch logic or any existing handler.
class CommandBus {
private final Map<Class<?>, CommandHandler<?>> handlers = new HashMap<>();
<C> void register(Class<C> type, CommandHandler<C> handler) { handlers.put(type, handler); }
@SuppressWarnings("unchecked")
<C> void dispatch(C command) {
CommandHandler<C> handler = (CommandHandler<C>) handlers.get(command.getClass());
if (handler == null) throw new IllegalStateException("No handler for " + command.getClass());
handler.handle(command);
}
}
51. Explain how to implement scheduling of deferred or delayed Command execution in Java, comparing java.util.Timer against ScheduledExecutorService, and note which is preferable for production use.
Both let you submit a command to run once, after a delay, or repeatedly, but Timer runs everything on a single background thread and, notoriously, if any scheduled task throws an uncaught exception, the entire Timer thread dies silently, canceling all future scheduled tasks. ScheduledExecutorService uses a pool of threads, isolates a single task's exception from other scheduled tasks, and integrates with the rest of the java.util.concurrent ecosystem, making it the preferred choice in production code.
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.schedule(() -> reminderCommand.execute(), 30, TimeUnit.MINUTES); // deferred, isolated
52. Describe a common bug where a Command captures a reference to mutable state instead of a snapshot of its value, causing it to execute against stale or unexpectedly changed data when it finally runs, and show the fix.
If a command's constructor stores a reference to a mutable object, such as a shopping cart or a list, rather than copying the values it actually needs at construction time, then any later mutation of that shared object before the command runs (especially likely if the command sits in a queue for a while) changes what the command sees when execute() finally fires, producing a result the caller never intended.
// Buggy: captures a live, mutable reference
class ApplyDiscountCommand implements Command {
private final List<Item> items; // same list instance the cart keeps mutating
ApplyDiscountCommand(List<Item> items) { this.items = items; }
public void execute() { items.forEach(Item::applyDiscount); } // may run on a different cart than intended
}
// Fixed: snapshot the values needed at construction time
class ApplyDiscountCommand implements Command {
private final List<Item> items;
ApplyDiscountCommand(List<Item> items) { this.items = List.copyOf(items); } // immutable snapshot
public void execute() { items.forEach(Item::applyDiscount); }
}
53. Design an undo history stack with a maximum size and an eviction policy, and explain the trade-off between keeping unlimited undo history and bounding memory usage.
An unbounded undo stack can grow without limit in a long-running session, each entry potentially holding a memento snapshot, eventually consuming significant memory for history the user will likely never actually revisit. A bounded stack caps the number of retained commands and evicts the oldest entry (from the bottom of the undo stack) once the cap is reached, trading away very old undo history for predictable memory usage.
class BoundedCommandHistory {
private final int maxSize;
private final Deque<UndoableCommand> undoStack = new ArrayDeque<>();
BoundedCommandHistory(int maxSize) { this.maxSize = maxSize; }
void execute(UndoableCommand command) {
command.execute();
undoStack.push(command);
if (undoStack.size() > maxSize) undoStack.removeLast(); // evict oldest
}
}
54. Explain how Java 8 lambdas can replace simple, stateless Command implementations, and show the same command expressed both as a full class and as a lambda assigned to a functional interface.
When a command's entire job is to call one method on one captured receiver, with no undo, no metadata, and no need for a dedicated named type, a lambda targeting the Command functional interface removes the boilerplate of a whole class while keeping the same decoupling between invoker and receiver.
interface Command { void execute(); } // a single abstract method -> lambda target
// As a class
class TurnOnLightCommand implements Command {
private final Light light;
TurnOnLightCommand(Light light) { this.light = light; }
public void execute() { light.turnOn(); }
}
// As a lambda, same behavior, no class needed
Command turnOn = () -> light.turnOn();
55. Explain how a method reference such as receiver::action can serve directly as a Command implementation, and identify the constraint on the method's signature for this to compile.
A bound instance method reference, light::turnOn, already has a receiver instance bound in (the object before the ::), and if the method takes no arguments and returns void, its signature matches Command's single abstract method exactly, so the reference itself can be assigned directly wherever a Command is expected, with no lambda body needed at all.
Light light = new Light();
Command turnOn = light::turnOn; // bound method reference, matches Command's execute() shape exactly
invoker.setCommand(turnOn);
56. Explain when lambdas and method references are not sufficient replacements for a full Command class, listing the specific capabilities that force a fallback to a named class.
Lambdas fall short the moment a command needs state beyond what it captured at creation and never changes (a mutable field it updates for undo bookkeeping), needs a second method such as undo() (a lambda can only implement a single abstract method's interface), needs to be reliably serialized for a persisted queue (lambda classes are JVM-implementation-specific and not a safe serialization target), or needs identity/equality semantics for deduplication.
57. Describe how the Axon Framework's CommandGateway realizes the Command pattern at framework scale for a CQRS/event-sourced Java application, and what it adds beyond a hand-rolled command bus.
Axon's CommandGateway is the invoker: application code sends a plain command object (typically an immutable POJO annotated to identify its target aggregate), and the gateway routes it, synchronously or asynchronously, to the @CommandHandler-annotated method on the matching aggregate, which validates it and emits events. Beyond a hand-rolled bus, Axon adds automatic routing by aggregate identifier, retry and timeout configuration, interceptor chains for cross-cutting concerns, and integration with its event store for the resulting event-sourced state changes.
commandGateway.send(new TransferFundsCommand(fromId, toId, amount));
// inside the Account aggregate
@CommandHandler
void handle(TransferFundsCommand command) {
apply(new FundsWithdrawnEvent(command.fromId(), command.amount()));
}
58. Explain how the Command pattern underlies input handling and replay systems in game development, where every player action is captured as a command object rather than applied directly.
Instead of a keypress directly mutating a character's state, the input layer translates it into a command object (MoveLeftCommand, JumpCommand, FireWeaponCommand) which is then applied to the game world's receiver objects. Because every action is an object, the game can log the exact sequence of commands issued during a session and later replay them frame-for-frame to reproduce a bug, power an instant replay feature, or drive a deterministic network simulation.
59. Design a macro-recording feature that captures a sequence of user-triggered Commands so they can be replayed later as a single reusable script, and explain what state each recorded command must preserve to replay correctly.
While recording, every command the user triggers is appended, in order, to a list rather than only being executed; "stop recording" freezes that list as a named macro, and "play macro" simply executes each stored command in sequence, exactly as a MacroCommand would (see Q18). For replay to be faithful, each recorded command must hold the exact parameters used originally (not a live reference to state that may have since changed, per Q52), so replaying it later reproduces the same action rather than adapting to whatever the current state happens to be.
class MacroRecorder {
private final List<Command> recorded = new ArrayList<>();
private boolean recording;
void record(Command command) {
command.execute();
if (recording) recorded.add(command);
}
Command finishRecordingAsMacro() { return new MacroCommand(List.copyOf(recorded)); }
}
60. Explain why some networked multiplayer games synchronize state by sending Command objects (player inputs) across the network rather than sending the resulting state directly, and what determinism requirement this places on command execution.
Sending full game state every tick is bandwidth-heavy; sending only the small command objects representing player intent ("move forward," "fire") is far cheaper, provided every client's simulation, given the same sequence of commands applied in the same order, produces bit-for-bit identical results (lockstep networking). This places a strict determinism requirement on command execution: no reliance on wall-clock time, unseeded randomness, or any other input not itself transmitted as part of the command stream, or clients will silently diverge.
61. Explain how commands map onto the steps of a Saga in a distributed transaction across microservices, and describe the role each service's compensating command plays if a later step fails.
Each step of a saga is naturally expressed as a command sent to one participating service (reserve inventory, charge payment, schedule shipping), and each of those commands has a paired compensating command that undoes its business effect if a subsequent step in the saga fails (release the reservation, refund the charge). Unlike Q12/Q13's in-process undo, saga compensation runs against a remote service, potentially much later, and must itself be idempotent since it may be retried.
interface SagaStep { void execute(); void compensate(); } // command + compensating command, remote-safe
62. Explain how the Unit of Work pattern relates to batching a group of Commands so they are all committed together, and how this differs from executing each command independently as it arrives.
Rather than each command committing its own change to the database immediately upon execution, a Unit of Work collects the pending changes from a batch of commands in memory and commits them together in a single transaction at the end of the unit, either all succeeding or all rolling back together. This is useful when several commands within one logical operation must be atomic as a group, something individually-committing commands cannot provide on their own.
UnitOfWork uow = new UnitOfWork();
uow.enlist(new UpdateInventoryCommand(sku, -1));
uow.enlist(new RecordSaleCommand(orderId, sku));
uow.commit(); // both applied atomically, or neither
63. Design a multi-step wizard UI where each step's changes are represented as a Command, and explain how the wizard supports going back (undoing a step) and forward (redoing) across step boundaries.
Each wizard step, on "Next," constructs and executes a command capturing exactly what that step changed in the underlying draft object, and pushes it onto the same kind of undo stack described in Q15; clicking "Back" calls undo() on the most recent step's command, cleanly reverting only that step's contribution, and clicking "Next" again after going back replays it via the redo stack unless the user changed their answer, in which case a new command replaces the old one (and the redo stack is cleared, per Q16).
64. Explain how the Command pattern makes keyboard shortcut remapping straightforward, letting a user rebind which key triggers a given action without touching the action's implementation at all.
Since a keybinding is just an invoker mapping a key combination to a Command instance, remapping a shortcut means updating that mapping (which key points to which command), never touching the command's own code. The same SaveDocumentCommand instance used by the menu and toolbar (see Q21) can be rebound from Ctrl+S to any other key combination purely by changing the invoker's lookup table.
Map<KeyStroke, Command> keyBindings = new HashMap<>();
keyBindings.put(KeyStroke.getKeyStroke("ctrl S"), saveCommand);
// remap: user changes the shortcut in settings
keyBindings.remove(KeyStroke.getKeyStroke("ctrl S"));
keyBindings.put(KeyStroke.getKeyStroke("ctrl shift S"), saveCommand); // same command instance
65. Design a command-line application where each subcommand (such as "init," "build," "deploy") is implemented as a Command object, and explain how the CLI's argument parser acts as the Invoker selecting which one to run.
Each subcommand is its own class implementing a shared CliCommand interface with an execute(String[] args) method; the parser reads the first token, looks it up in a registry mapping subcommand names to command instances, and dispatches to whichever one matches, remaining completely agnostic to what "build" or "deploy" actually do.
Map<String, CliCommand> subcommands = Map.of(
"init", new InitCommand(),
"build", new BuildCommand(),
"deploy", new DeployCommand()
);
CliCommand command = subcommands.get(args[0]);
command.execute(Arrays.copyOfRange(args, 1, args.length));
66. Explain how build tools such as Gradle model tasks in a way that resembles the Command pattern, including how task dependencies relate to a receiver's precondition state.
A Gradle task is, structurally, a named command object with an execute-equivalent action closure, a defined set of inputs and outputs, and declared dependencies on other tasks that must run first, functioning much like an invoker-orchestrated sequence of commands each of which knows what "receiver state" (files, prior task outputs) it needs to have been produced before it can safely run. Gradle's task graph resolves the correct order automatically, similar to how a saga's steps must run in dependency order.
67. Explain how the Quartz job scheduling library's Job interface realizes the Command pattern, and how a JobDetail plus a Trigger together play the role of binding a command to its scheduled invocation.
Quartz's Job interface, with its single execute(JobExecutionContext) method, is directly analogous to Command's execute(); a JobDetail describes which concrete Job class to instantiate and what data map (arguments) to give it, playing the ConcreteCommand-construction role, while a Trigger is the invoker, deciding when that job actually fires (a cron schedule, a one-time delay), completely decoupled from what the job itself does.
public class SendReportJob implements Job {
@Override
public void execute(JobExecutionContext context) { reportService.sendDailyReport(); }
}
JobDetail job = JobBuilder.newJob(SendReportJob.class).build();
Trigger trigger = TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(6, 0)).build();
68. Discuss how an HTTP request handler in a web framework can be thought of as an Invoker dispatching to a Command-like controller action, and where the analogy to the classic Command pattern breaks down.
A dispatcher servlet or router (the invoker) receives an incoming HTTP request and, based on the URL and method, calls the matching controller method (the ConcreteCommand-like action), without knowing what that action actually does to the domain model, which mirrors Command's decoupling nicely. The analogy weakens because a typical controller method is not usually a separate object bound to its receiver ahead of time; it is more often a plain method invoked with fresh arguments extracted per request, and controllers rarely support the "store it, undo it, replay it later" capabilities that motivate a true Command object.
69. Explain the design principle of separating command (write) endpoints from query (read) endpoints in a REST API, and how this separation echoes the intent behind treating write operations as first-class Command objects.
Command-style write endpoints (POST /orders, POST /orders/{id}/cancel) represent an intent to change state and are naturally modeled, validated, and audited as discrete request objects, much like a GoF Command's bound intent-plus-arguments; query endpoints (GET /orders/{id}) simply read and return current state with no side effects and nothing to log as an "action taken." Keeping the two conceptually and often physically separate (as in CQRS, see Q49) lets write paths add validation, idempotency keys, and auditing without query paths carrying that same overhead.
70. Explain how java.util.function.Supplier and Consumer relate to the Command pattern's shape, and identify a case where one is a more natural fit than a Command or Runnable.
Supplier<T> (no input, produces a value) is closer to Callable<V> than to a classic void-returning Command; Consumer<T> (takes an input, no return) is a natural fit when the "command" needs an argument supplied at execution time rather than bound at construction, such as an event handler receiving the triggering event object.
Consumer<ClickEvent> onClick = event -> auditLog.record("clicked", event.source());
button.addClickListener(onClick); // the invoker supplies the argument at trigger time, not at construction
Choose Consumer<T> over a fully-bound Command when the same handler logic legitimately needs different input each time it fires, rather than being pre-bound to one fixed receiver and argument set.
71. Discuss where exception handling should occur for a Command whose execute() method can fail: inside execute() itself, inside the Invoker, or by the caller of the Invoker, and justify the choice.
The command itself should only catch and translate exceptions it has enough context to meaningfully handle or wrap (translating a receiver's checked exception into a documented unchecked one, as with an Adapter); it should not swallow failures silently, since that hides real problems from whoever is coordinating command history or a retry policy. The invoker, if it is generic (like a queue-based worker or a command bus), should treat any exception from execute() as a signal to route the command to logging, retry, or a dead-letter queue, rather than crashing the whole processing loop.
72. Explain the conceptual difference between "undo" and "compensating action," and why a saga step's compensating command is not simply that step's undo() method.
Undo assumes you can cleanly and immediately reverse an in-process action, typically because nothing external and irreversible has happened yet. A compensating action acknowledges that the original action may have already had real-world, possibly irreversible side effects (an email was sent, a shipment was dispatched) by the time compensation runs, so instead of pretending it never happened, it performs a new, forward action that semantically counteracts the effect (send a cancellation notice, issue a refund) rather than literally rewinding time.
73. Compare Saga-style compensation against classic in-process Command.undo() along the dimensions of timing, reliability guarantees, and whether the original effect can truly be erased.
| Dimension | Command.undo() | Saga compensation |
|---|---|---|
| Timing | Typically immediate, in the same process, often within milliseconds of execute(). | Can happen much later, across a network, after other services have already reacted. |
| Reliability guarantee | Assumed to succeed synchronously; failure usually means a programming bug. | Must itself be retried and made idempotent; the remote service may be temporarily unavailable. |
| Can the effect truly be erased? | Usually yes — nothing external has observed or acted on the change yet. | Often no — the effect must be counteracted, not erased, since other systems may have already reacted to it. |
74. Discuss the considerations for making a Command class Serializable so it can be persisted to disk or placed on a message queue, and what fields should be excluded from serialization.
Only include fields representing plain data (identifiers, primitive values, immutable value objects) needed to reconstruct and re-execute the command later; explicitly exclude, via transient, any field holding a live resource, an open connection, or a direct in-process object reference to the receiver, since those cannot meaningfully survive serialization and must instead be re-resolved (by ID, via dependency lookup) when the command is deserialized on the consuming side.
class TransferFundsCommand implements Serializable {
private final String fromAccountId; // ID, not a live Account reference
private final String toAccountId;
private final BigDecimal amount;
// receiver (Account) is looked up by ID on the consumer side, never serialized directly
}
75. Explain the challenges of versioning a serialized Command's schema over time as new fields are added or old ones are removed, given that older, already-queued commands may still be in flight when a new deployment rolls out.
A command's serialized shape is effectively a public contract between whichever version of the code produced it and whichever version consumes it, and those two versions can legitimately differ during a rolling deployment. Add new fields as optional with sensible defaults so an older producer's command still deserializes correctly; avoid removing or renaming a field outright, and instead deprecate it while a consumer temporarily supports both the old and new shape, similar to how an Adapter isolates an API version difference (see the sibling Adapter article's Q60).
76. Explain the code smell of business logic leaking into the Invoker, such as an invoker that inspects a command's type or fields before deciding whether to call execute(), and how to refactor it away.
If an invoker contains a conditional that branches on what kind of command it holds, or reaches into a command's fields to make a business decision, the invoker has stopped being generic and has absorbed logic that belongs either inside the command's own canExecute()/execute() or inside the receiver. Refactor by moving that decision into the command itself, so the invoker's call remains a single, uniform command.execute() regardless of which concrete command it holds.
// Smell: invoker branching on command type
if (command instanceof RefundCommand rc && rc.amount().compareTo(LIMIT) > 0) { requireApproval(); }
else { command.execute(); }
// Better: the decision belongs to the command itself
command.execute(); // RefundCommand.execute() internally checks its own approval requirement
77. Explain the "god command" anti-pattern, where a single Command class grows to encapsulate many unrelated operations behind conditional branches, and how to refactor it into properly separated commands.
A god command accumulates an ever-growing set of optional fields and an execute() full of conditionals dispatching to different behavior depending on which fields are populated, effectively becoming several different commands wearing one class's clothing. This defeats the pattern's purpose: it cannot be logged, undone, or reasoned about as one coherent unit of work, since "what it actually did" varies by which branch ran.
Refactor by splitting it into one focused command class per distinct operation, each with its own narrow set of fields and its own straightforward, branch-free execute(), and route to the correct one via a command bus (see Q50) rather than a single class's internal conditionals.
78. Explain how the Null Object pattern applies to Command implementations, using a NoOpCommand to fill an "empty" menu slot or unbound keyboard shortcut instead of leaving a null reference.
Rather than an invoker having to null-check its command reference before every call (and remembering to do so consistently everywhere), a shared NoOpCommand instance whose execute() simply does nothing can be used as the default for an unbound slot, letting invoker code call command.execute() unconditionally and safely.
class NoOpCommand implements Command {
static final NoOpCommand INSTANCE = new NoOpCommand();
private NoOpCommand() {}
@Override public void execute() { /* intentionally does nothing */ }
}
Command slot = NoOpCommand.INSTANCE; // safe default, no null check needed at call sites
79. Explain how to decorate a Command with cross-cutting behavior, such as timing, retry, or logging, without modifying the underlying ConcreteCommand's own execute() implementation.
Because decoration and Command share the "same interface" structural idea (see the sibling Adapter article's Q7 for the general Adapter-versus-Decorator distinction), a decorator implementing Command and wrapping another Command instance can add behavior before and after delegating to execute(), and decorators can be stacked (timing wrapping retry wrapping logging wrapping the real command) without the innermost command ever knowing it is wrapped.
class TimingCommandDecorator implements Command {
private final Command delegate;
TimingCommandDecorator(Command delegate) { this.delegate = delegate; }
@Override
public void execute() {
long start = System.nanoTime();
delegate.execute();
log.info("Command took {} ms", (System.nanoTime() - start) / 1_000_000);
}
}
80. Design a Command whose successful execution triggers Observer-style notifications to interested listeners, and explain how to keep the command itself unaware of exactly who is listening.
Rather than the command directly calling specific listener methods, its receiver (or the command itself, via an injected event publisher) fires a domain event after the state change succeeds, and any number of independently-registered observers react to that event without the command needing a reference to any of them by name.
class PlaceOrderCommand implements Command {
private final Order order;
private final ApplicationEventPublisher publisher;
PlaceOrderCommand(Order order, ApplicationEventPublisher publisher) {
this.order = order; this.publisher = publisher;
}
@Override
public void execute() {
order.place();
publisher.publishEvent(new OrderPlacedEvent(order.id())); // observers subscribe independently
}
}
81. Design an interceptor (middleware) chain that runs around every Command dispatched through a command bus, handling concerns like authorization and validation before the command's actual handler runs.
Each interceptor implements a common signature accepting the command and a reference to "the next step in the chain," so a chain of interceptors (authorization, then validation, then logging) can each decide to short-circuit (reject the command) or call through to the next interceptor, with the final link in the chain being the actual command handler, structurally the Chain of Responsibility pattern wrapped around Command dispatch (see Q40).
interface CommandInterceptor { void intercept(Object command, CommandChain chain); }
class AuthorizationInterceptor implements CommandInterceptor {
@Override
public void intercept(Object command, CommandChain chain) {
if (!authorizer.isAllowed(command)) throw new UnauthorizedException();
chain.proceed(command); // pass to the next interceptor, eventually the handler
}
}
82. Design a command handler for an event-driven microservice that consumes commands from a message broker topic and dispatches each to the correct domain aggregate, including how failures are surfaced back to the caller if the interaction is meant to feel synchronous.
The consumer deserializes each incoming message into its typed command, resolves the target aggregate by an identifier embedded in the command, invokes the aggregate's corresponding method, and persists the result; if callers expect a synchronous-feeling response (common in a request/response-over-messaging setup), the consumer publishes a correlated reply message to a response topic keyed by a correlation ID the original caller is waiting on.
@KafkaListener(topics = "account-commands")
void onCommand(ConsumerRecord<String, String> record) {
TransferFundsCommand command = deserialize(record.value(), TransferFundsCommand.class);
try {
accountService.transfer(command);
replyPublisher.success(command.correlationId());
} catch (InsufficientFundsException ex) {
replyPublisher.failure(command.correlationId(), ex.getMessage());
}
}
83. Discuss how to achieve effectively-exactly-once processing of commands consumed from a broker by combining a transactional outbox on the producer side with an idempotent consumer on the receiving side.
The producer's outbox (Q29) guarantees the command is durably recorded and eventually published even across a crash, giving at-least-once delivery on the send side; the consumer's idempotency check (Q32, Q33) guarantees that redelivered duplicates have no additional effect. Neither half alone achieves exactly-once semantics, but combined they produce an outcome indistinguishable from exactly-once: every command is guaranteed to be applied, and applied exactly one time in its observable effect, even though the underlying transport may deliver it more than once.
84. Walk through the full lifecycle of a Command in a CQRS/event-sourced aggregate: how it is validated against the aggregate's current state, what events it produces on success, and how those events are applied to update in-memory state.
The command bus dispatches the command to the target aggregate's handler method, which first checks the command's preconditions against the aggregate's current in-memory state (reconstructed by replaying its prior events); if valid, the handler does not mutate state directly but instead raises one or more events describing what happened, and a separate "apply" step updates the aggregate's in-memory fields from those events, the same apply logic used both immediately after handling and when replaying history from the event store.
@CommandHandler
void handle(WithdrawCommand command) {
if (balance.compareTo(command.amount()) < 0) throw new InsufficientFundsException();
apply(new FundsWithdrawnEvent(command.accountId(), command.amount())); // raises, then applies
}
@EventSourcingHandler
void on(FundsWithdrawnEvent event) { this.balance = this.balance.subtract(event.amount()); }
85. Explain how to write a test that verifies a Command's undo() genuinely restores the receiver to its exact prior state, rather than merely appearing to succeed without side effects.
Capture a deep snapshot (or an equals()-comparable representation) of the receiver's relevant state before calling execute(), run execute() then undo(), and assert the receiver's state afterward equals the original snapshot field-by-field, rather than only asserting that undo() ran without throwing.
@Test
void undoRestoresExactPriorBalance() {
Account account = new Account("acc-1", new BigDecimal("100.00"));
BigDecimal before = account.getBalance();
WithdrawCommand command = new WithdrawCommand(account, new BigDecimal("30.00"));
command.execute();
command.undo();
assertThat(account.getBalance()).isEqualByComparingTo(before); // exact restoration, not just "no error"
}
86. Discuss the memory considerations of an undo history that stores full Memento snapshots for every command, in a long-running desktop application editing a large document, and how you would reduce that footprint.
If every entry in a deep undo history holds a full snapshot of a large document, memory usage grows linearly with history length and document size, which can become significant in a long editing session. Reduce it by preferring inverse-operation commands (Q12) over full snapshots wherever the change is small and simple, storing snapshots only for genuinely complex operations, compressing older snapshots, or capping history size (Q53) so the oldest, least-likely-to-be-revisited entries are evicted first.
87. Explain how to coalesce a run of consecutive, closely-related Commands, such as individual keystroke insertions, into a single undo step so the user does not have to press undo once per character typed.
Rather than pushing a new command onto the history for every keystroke, check whether the incoming command is "mergeable" with the most recently pushed one (same operation type, contiguous position, within a short time window), and if so, extend the existing command's recorded range instead of pushing a new stack entry, so one undo reverts an entire burst of typing rather than a single character.
void execute(InsertTextCommand command) {
UndoableCommand top = undoStack.peek();
if (top instanceof InsertTextCommand last && last.isAdjacentAndRecent(command)) {
last.extend(command); // merge into the previous entry, no new stack push
} else {
command.execute();
undoStack.push(command);
}
}
88. Design a Command that carries a precondition guard function, letting the same Command class enforce different execution conditions per instance without subclassing.
Rather than hardcoding a fixed precondition inside canExecute(), accept a BooleanSupplier (or a Predicate over the receiver) at construction time, so the same generic command class can be configured with different guard logic per instance, similar in spirit to the pluggable adapter idea (see the sibling Adapter article's Q25) applied to Command.
class GuardedCommand implements Command {
private final Command delegate;
private final BooleanSupplier guard;
GuardedCommand(Command delegate, BooleanSupplier guard) { this.delegate = delegate; this.guard = guard; }
@Override
public void execute() {
if (!guard.getAsBoolean()) throw new IllegalStateException("Precondition not met");
delegate.execute();
}
}
89. Discuss what changes are needed for Commands to be safely executed against a shared Receiver from multiple concurrent invokers, such as several UI panels or several worker threads all able to trigger commands against the same underlying model.
If multiple invokers can trigger commands against the same receiver concurrently, the receiver's own state mutations must be made thread-safe (synchronized methods, an internal lock, or a thread-safe data structure), since Command's decoupling says nothing by itself about concurrent access; additionally, a shared undo/redo history stack becomes a point of contention that itself needs synchronization, or a re-think toward a per-session history if commands from different actors should not interleave in one shared undo stack at all.
90. Design a bounded Command queue with an explicit backpressure and rejection policy for when producers submit commands faster than consumers can process them.
A bounded queue (such as ArrayBlockingQueue with a fixed capacity) prevents unbounded memory growth when producers outpace consumers, but requires an explicit policy for what happens when it is full: block the producer until space frees up, reject the new command immediately with a signal the producer must handle, or discard the oldest queued command to make room, each trading a different kind of harm for a different guarantee.
BlockingQueue<Command> queue = new ArrayBlockingQueue<>(1000);
boolean submit(Command command) {
return queue.offer(command); // false if full -- caller decides: reject, retry later, or shed load
}
91. Explain how a RejectedExecutionHandler on a ThreadPoolExecutor is itself an invoker-level policy decision for what happens to a Command that cannot be accepted, and describe the built-in policies the JDK provides.
When a bounded ThreadPoolExecutor's work queue is full and no thread is available, the RejectedExecutionHandler decides what happens to the rejected command, entirely separate from what the command itself does; the JDK ships AbortPolicy (throws, the default), CallerRunsPolicy (runs the command on the submitting thread itself, providing natural backpressure), DiscardPolicy (silently drops it), and DiscardOldestPolicy (evicts the oldest queued command to make room for the new one).
new ThreadPoolExecutor(4, 4, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()); // invoker-level rejection policy, not command-level logic
92. Design a generic Command<R> interface parameterized on a result type, and explain how it lets an invoker retrieve a typed outcome after execution without every command sharing a single fixed return type.
Parameterizing the interface on R lets each concrete command declare exactly what kind of result its execute() produces, at compile time, rather than every command in the system being forced into a single shared return type (or none at all, as with plain void Command).
interface Command<R> {
R execute();
}
class FetchOrderCommand implements Command<Order> {
private final String orderId;
FetchOrderCommand(String orderId) { this.orderId = orderId; }
@Override public Order execute() { return orderRepository.findById(orderId); }
}
Order order = new FetchOrderCommand("ord-1").execute(); // typed result, no cast needed
93. Compare using java.util.concurrent.Callable<V> directly against defining a custom Command<R> interface (from Q92) for result-producing commands, and explain when the custom interface earns its keep over the JDK type.
| Aspect | Callable<V> | Custom Command<R> |
|---|---|---|
| Interoperability | Works directly with ExecutorService.submit() and Future<V> out of the box. | Needs an adapter or manual wiring to submit through an ExecutorService. |
| Checked exceptions | call() throws Exception -- broad, forces callers to handle a generic checked type. | Can declare a narrower, domain-specific checked or unchecked exception. |
| Extra methods (undo, canExecute, metadata) | Not possible -- Callable is a single-method functional interface. | Freely extendable with undo(), canExecute(), or descriptive fields. |
Use Callable<V> when the command's only job is to run on an executor and return a value; reach for a custom Command<R> the moment you need any capability beyond that single method.
94. Discuss how Spring's @Async methods and ApplicationEventPublisher can be combined to implement a lightweight Command-style dispatch mechanism without writing a custom command bus.
An @Async-annotated service method already behaves like an invoker submitting a command to a background thread pool managed by Spring, decoupling the caller from when the work actually runs; combined with ApplicationEventPublisher, a caller can publish a plain event object representing intent (functioning much like a CQRS-style command message, see Q49), and one or more @EventListener methods, some of them @Async, react to it, giving a workable command-dispatch mechanism using only framework annotations.
@Async
@EventListener
void onOrderPlaced(OrderPlacedEvent event) { emailService.sendConfirmation(event.orderId()); }
applicationEventPublisher.publishEvent(new OrderPlacedEvent(orderId)); // fire-and-forget dispatch
95. Design a bank transfer feature using the Command pattern that supports both execute() for the transfer and a safe compensating action if the transfer must be reversed after the fact due to a downstream failure.
The command captures the source account, destination account, and amount, executes the withdrawal and deposit atomically within one transaction, and exposes a compensating reverse() operation (distinct from an in-memory undo(), per Q72) that performs a new forward transfer in the opposite direction rather than attempting to erase the original transaction, since by the time reversal is needed, downstream systems (statements, notifications) may already have observed the original transfer.
class TransferFundsCommand implements Command {
// ... fromAccountId, toAccountId, amount, idempotencyKey
@Override
public void execute() { /* atomic withdraw + deposit, guarded by idempotencyKey */ }
TransferFundsCommand reversal() {
return new TransferFundsCommand(toAccountId, fromAccountId, amount, idempotencyKey + "-reversal");
}
}
96. Design a smart-home automation system where triggers (a schedule, a sensor event, a voice command) each construct and dispatch the same underlying Command objects against device receivers such as lights and thermostats.
Define device-facing commands (SetThermostatCommand, TurnOnLightCommand) bound to their specific device receiver, and let entirely different trigger mechanisms, a cron-like schedule, a motion sensor callback, or a voice assistant's intent parser, all construct and hand off the same command types to a shared dispatcher, so adding a new trigger source (say, a new voice assistant integration) requires no changes to how devices actually respond.
Command eveningLights = new TurnOnLightCommand(livingRoomLight);
scheduleTrigger.at("18:00", eveningLights);
voiceTrigger.onIntent("turn on the lights", eveningLights); // same command, different trigger
97. Discuss the relationship between a database transaction's commit/rollback semantics and Command's execute()/undo(), and explain why a database transaction is not itself simply "a Command with built-in undo."
A transaction's rollback only works before commit, undoing uncommitted, in-flight changes still held in the transaction log or buffer; once committed, there is no built-in "undo the transaction" operation, only the option to execute a new, separate compensating transaction that reverses the effect going forward, which is conceptually the same distinction as compensation versus undo (Q72). Command's undo() typically models the pre-commit case (an in-memory action not yet durably finalized), while a compensating command models the post-commit case.
98. Explain how storing a complete log of executed Commands enables replaying them against a fresh environment to reproduce a production bug, and what determinism requirements this debugging technique depends on.
If every command that ran in production, in order, with its exact arguments, was recorded (much like the audit log in Q47 or the macro recorder in Q59), a developer can replay that exact sequence against a copy of the pre-incident state in a debugger, stepping through each command's execute() to see exactly where behavior diverges from expectations. This technique only works if command execution is deterministic given the same inputs (see the network-game determinism trap in Q60); a command that reads non-deterministic external state during execute() will not reproduce the original bug faithfully on replay.
99. Explain the challenges of replaying an old, previously-logged Command against a Receiver whose business logic has since changed, and how you would decide whether to replay it as-is or reject it as no longer valid.
A command logged months ago may have been valid under the business rules and schema in effect at that time, but replaying it verbatim against today's receiver logic (with new validation rules, different fee calculations, or a changed schema) can produce a different, potentially incorrect result compared to what actually happened originally. The safest approach is to version commands (see Q75) so a replay engine can select the receiver logic version matching the command's own recorded version, or explicitly flag commands whose semantics have since changed as non-replayable without manual review.
100. In a system design interview, how would you decide whether a given feature genuinely calls for the Command pattern versus a direct method call, and what is the single strongest signal that separates the two?
The single strongest signal is whether anything needs to happen to the request between the moment it is decided upon and the moment it actually runs: stored, queued, logged, retried, undone, replayed, or triggered from more than one place without duplicating logic. If the answer is "no, it should just run immediately and once," a direct method call is simpler, has one less class to navigate, and is easier for a reader to trace end to end.
A strong interview answer names this test explicitly rather than reaching for Command by reflex: "do I need to decouple deciding what to do from when, where, or how many times it runs?" is the question that actually distinguishes a genuine Command use case from an over-engineered one, and naming the specific capability (undo, a queue, an audit trail) you actually need is far more convincing than citing the pattern by name.
Post a Comment
Add