Undo, redo, job queues and retries are all the same move: turn the request into an object, so it can be held, logged, repeated and reversed.
Type a shopping list in a notes app, delete a word, and press undo. The word comes back. Press undo again and your last line of typing disappears; press redo and it returns. Nothing in the app remembers how it did those things. It remembers what you asked for, one request at a time, in a list.
That is the Command pattern: wrap a request (do this, with these arguments) in an object. Once a request is an object, you can do things a method call can't: keep it for later, put it in a queue, write it to a log, run it again, and, if it knows how, reverse it. It's the pattern behind undo in the text editor, the move history in chess and the input queue of the order book, and you use it whenever you hand a job to a worker instead of doing it yourself.
This lesson has two halves on purpose. First an undo and redo history for a notes app, which shows what a command must carry to reverse itself. Then a queue of payout jobs, which shows what a command must carry to be retried without paying twice. Both are built in Python, Java, C++ and JavaScript.
A waiter's order slip is a command: the request written down as an object, so it can wait on a rail, be logged, or be struck off.
The first version: undo bolted on#
The first version of the notes app has type and delete. Then somebody asks for undo, and the quickest thing is to remember the last action and branch on what it was:
# notes_v1.py: undo is bolted on: remember the last action, and branch on its kind
class Notes:
def __init__(self) -> None:
self.text = ""
self.last = None # ("type", at, typed text) or ("delete", at, removed text)
def type(self, at: int, s: str) -> None:
self.text = self.text[:at] + s + self.text[at:]
self.last = ("type", at, s)
def delete(self, at: int, count: int) -> None:
removed = self.text[at:at + count]
self.text = self.text[:at] + self.text[at + count:]
self.last = ("delete", at, removed)
def undo(self) -> None:
if self.last is None:
return
kind, at, data = self.last
if kind == "type":
self.text = self.text[:at] + self.text[at + len(data):]
elif kind == "delete":
self.text = self.text[:at] + data + self.text[at:]
self.last = None # one step back, and that is all
notes = Notes()
notes.type(0, "Buy milk")
notes.type(8, " and eggs")
print(f"'{notes.text}'")
notes.undo()
print(f"'{notes.text}'")
notes.undo()
print(f"'{notes.text}'")// NotesV1.java: undo is bolted on: remember the last action, and branch on its kind
class NotesV1 {
record Last(String kind, int at, String data) {} // "type" with the typed text, or "delete" with the removed text
static class Notes {
String text = "";
private Last last = null;
void type(int at, String s) {
text = text.substring(0, at) + s + text.substring(at);
last = new Last("type", at, s);
}
void delete(int at, int count) {
String removed = text.substring(at, at + count);
text = text.substring(0, at) + text.substring(at + count);
last = new Last("delete", at, removed);
}
void undo() {
if (last == null) return;
if (last.kind().equals("type")) {
text = text.substring(0, last.at()) + text.substring(last.at() + last.data().length());
} else if (last.kind().equals("delete")) {
text = text.substring(0, last.at()) + last.data() + text.substring(last.at());
}
last = null; // one step back, and that is all
}
}
public static void main(String[] args) {
Notes notes = new Notes();
notes.type(0, "Buy milk");
notes.type(8, " and eggs");
System.out.println("'" + notes.text + "'");
notes.undo();
System.out.println("'" + notes.text + "'");
notes.undo();
System.out.println("'" + notes.text + "'");
}
}// notes_v1.cpp: undo is bolted on: remember the last action, and branch on its kind
#include <iostream>
#include <string>
class Notes {
std::string lastKind_; // "type", "delete", or empty
size_t lastAt_ = 0;
std::string lastData_; // the typed text, or the removed text
public:
std::string text;
void type(size_t at, const std::string& s) {
text.insert(at, s);
lastKind_ = "type", lastAt_ = at, lastData_ = s;
}
void del(size_t at, size_t count) {
lastKind_ = "delete", lastAt_ = at, lastData_ = text.substr(at, count);
text.erase(at, count);
}
void undo() {
if (lastKind_.empty()) return;
if (lastKind_ == "type") {
text.erase(lastAt_, lastData_.size());
} else if (lastKind_ == "delete") {
text.insert(lastAt_, lastData_);
}
lastKind_.clear(); // one step back, and that is all
}
};
int main() {
Notes notes;
notes.type(0, "Buy milk");
notes.type(8, " and eggs");
std::cout << "'" << notes.text << "'\n";
notes.undo();
std::cout << "'" << notes.text << "'\n";
notes.undo();
std::cout << "'" << notes.text << "'\n";
}// notesV1.js: undo is bolted on: remember the last action, and branch on its kind
class Notes {
constructor() {
this.text = "";
this.last = null; // { kind: "type" | "delete", at, data } (the typed text, or the removed text)
}
type(at, s) {
this.text = this.text.slice(0, at) + s + this.text.slice(at);
this.last = { kind: "type", at, data: s };
}
delete(at, count) {
const removed = this.text.slice(at, at + count);
this.text = this.text.slice(0, at) + this.text.slice(at + count);
this.last = { kind: "delete", at, data: removed };
}
undo() {
if (this.last === null) return;
const { kind, at, data } = this.last;
if (kind === "type") {
this.text = this.text.slice(0, at) + this.text.slice(at + data.length);
} else if (kind === "delete") {
this.text = this.text.slice(0, at) + data + this.text.slice(at);
}
this.last = null; // one step back, and that is all
}
}
const notes = new Notes();
notes.type(0, "Buy milk");
notes.type(8, " and eggs");
console.log(`'${notes.text}'`);
notes.undo();
console.log(`'${notes.text}'`);
notes.undo();
console.log(`'${notes.text}'`);'Buy milk and eggs'
'Buy milk'
'Buy milk'The second undo did nothing, because only the last action was remembered. It works for one step, and here is what the next month does to it:
- One step back, and that is all. To go further,
lastbecomes a list, and the code that undoes each kind of action is still oneifladder. - Every new kind of edit edits
undo. Paste, uppercase, replace-all: each is a new branch in the method that already undoes every other action. - No redo. The undone action was thrown away, so there is nothing to put back.
- The request itself is lost. What happened is a tuple like
("delete", 4, "milk "). It can't be queued, logged under a readable name or replayed. - Do and undo live in different places. The code that removes a word and the code that puts it back are in two methods that must be kept in step by hand.
Before the refactor: what keeps changing?
Look at Notes. Which part will keep changing as the app grows, and which part stays the same?
One strong answer
The kinds of edit change; the way they are handled doesn't. Paste, uppercase and replace-all will keep arriving. What stays the same is the routine around them: do the edit, remember it, reverse it if asked. So each edit becomes an object with the same two methods (execute and undo), and the routine is written once, against the interface.
The pattern#
The book that named the pattern (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states its intent in one sentence: "Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations."
In the notes app that gives five roles:
| Role | In the notes app | Its one job |
|---|---|---|
| Command (the interface) | Command with execute(doc) and undo(doc) |
Say what every request can do |
| Concrete commands | Insert, Delete, Uppercase, Macro |
Each is one kind of request, with its own arguments |
| Receiver | Doc |
The thing that actually changes |
| Invoker | Editor |
Runs commands and keeps the two stacks; never looks inside one |
| Client | the button handler | Builds the command with its arguments and hands it over |
%% title: Command: notes edits that undo
classDiagram
class Editor {
-undo_stack list
-redo_stack list
+run(cmd)
+undo() bool
+redo() bool
}
class Command {
<<interface>>
+execute(doc)
+undo(doc)
}
class Insert {
+at int
+text str
}
class Delete {
+at int
+count int
+removed str
}
class Uppercase {
+before str
}
class Macro {
+commands list
}
class Doc {
+text str
}
Editor "1" o-- "many" Command : history
Command <|.. Insert
Command <|.. Delete
Command <|.. Uppercase
Command <|.. Macro
Macro "1" o-- "many" Command
Command ..> Doc : changesRead the arrows. Editor holds a list of Command and has no arrow to Insert or Delete, so it compiles without knowing a single kind of edit. Each command depends on the Doc it changes, and a command like Delete carries the state it needs to reverse itself (the field removed). Macro is a command that holds other commands, which is how several edits become one undo step: the same shape as a composite (dp-13).
A command is an interface with execute and undo; the invoker runs commands without looking inside, and the receiver is the thing that really changes.
The code#
Each command has an execute and an undo. The editor runs a command, pushes it on the undo stack and clears the redo stack; undo moves a command from one stack to the other, and redo moves it back. The method is execute and not do because do is a reserved word in Java, C++ and JavaScript.
# notes_commands.py: every edit is an object that can do itself and undo itself
from abc import ABC, abstractmethod
from typing import List
class Doc:
"""The receiver: the thing the commands change."""
def __init__(self) -> None:
self.text = ""
class Command(ABC):
@abstractmethod
def execute(self, doc: Doc) -> None: ...
@abstractmethod
def undo(self, doc: Doc) -> None: ...
class Insert(Command):
"""Its own data is enough to reverse it: remove what it added."""
def __init__(self, at: int, text: str) -> None:
self.at, self.text = at, text
def execute(self, doc: Doc) -> None:
if not 0 <= self.at <= len(doc.text):
raise ValueError("insert outside the text")
doc.text = doc.text[:self.at] + self.text + doc.text[self.at:]
def undo(self, doc: Doc) -> None:
doc.text = doc.text[:self.at] + doc.text[self.at + len(self.text):]
class Delete(Command):
"""Needs stored state: what it removes is only known when it runs."""
def __init__(self, at: int, count: int) -> None:
self.at, self.count, self.removed = at, count, ""
def execute(self, doc: Doc) -> None:
if self.at < 0 or self.at + self.count > len(doc.text):
raise ValueError("delete past the end")
self.removed = doc.text[self.at:self.at + self.count]
doc.text = doc.text[:self.at] + doc.text[self.at + self.count:]
def undo(self, doc: Doc) -> None:
doc.text = doc.text[:self.at] + self.removed + doc.text[self.at:]
class Uppercase(Command):
"""No formula turns 'BUY' back into 'Buy', so it stores the text it replaced."""
def __init__(self) -> None:
self.before = ""
def execute(self, doc: Doc) -> None:
self.before = doc.text
doc.text = doc.text.upper()
def undo(self, doc: Doc) -> None:
doc.text = self.before
class Macro(Command):
"""Several commands that undo as one step: run in order, undo in reverse. All or nothing."""
def __init__(self, commands: List[Command]) -> None:
self.commands = commands
def execute(self, doc: Doc) -> None:
done: List[Command] = []
try:
for c in self.commands:
c.execute(doc)
done.append(c)
except Exception:
for c in reversed(done): # roll back what already ran
c.undo(doc)
raise
def undo(self, doc: Doc) -> None:
for c in reversed(self.commands):
c.undo(doc)
class Editor:
"""The invoker: it runs commands and keeps the undo and redo stacks."""
def __init__(self, limit: int = 100) -> None:
self.doc, self.limit = Doc(), limit
self._undo: List[Command] = []
self._redo: List[Command] = []
def run(self, cmd: Command) -> None:
cmd.execute(self.doc) # if this raises, nothing is recorded
self._undo.append(cmd)
self._redo.clear() # a new edit ends the redo history
if len(self._undo) > self.limit:
self._undo.pop(0) # the oldest step falls off
def undo(self) -> bool:
if not self._undo:
return False
cmd = self._undo.pop()
cmd.undo(self.doc)
self._redo.append(cmd)
return True
def redo(self) -> bool:
if not self._redo:
return False
cmd = self._redo.pop()
cmd.execute(self.doc)
self._undo.append(cmd)
return True
def show(step: str, ed: Editor) -> None:
print(f"{step}: '{ed.doc.text}'")
ed = Editor()
ed.run(Insert(0, "Buy milk"))
show("insert 'Buy milk'", ed)
ed.run(Insert(8, " and eggs"))
show("insert ' and eggs'", ed)
ed.run(Delete(4, 5))
show("delete 'milk '", ed)
ed.run(Uppercase())
show("uppercase", ed)
ed.undo()
show("undo", ed)
ed.undo()
show("undo", ed)
ed.redo()
show("redo", ed)
ed.run(Macro([Delete(4, 3), Insert(4, "&")]))
show("macro: replace 'and' with '&'", ed)
print("redo:", "done" if ed.redo() else "nothing to redo")
for bad in [Delete(40, 2), Macro([Insert(0, "X"), Delete(99, 1)])]:
try:
ed.run(bad)
except ValueError as err:
print(f"rejected ({err}): '{ed.doc.text}'")
ed.undo()
show("undo", ed)
small = Editor(limit=2)
for i, ch in enumerate("abc"):
small.run(Insert(i, ch))
print(f"-- history limit 2: '{small.doc.text}'")
for _ in range(3):
ok = small.undo()
print("undo:", f"'{small.doc.text}'" if ok else "nothing to undo")// NotesCommands.java: every edit is an object that can do itself and undo itself
import java.util.ArrayList;
import java.util.List;
class NotesCommands {
/** The receiver: the thing the commands change. */
static final class Doc {
String text = "";
}
interface Command {
void execute(Doc doc);
void undo(Doc doc);
}
/** Its own data is enough to reverse it: remove what it added. */
static final class Insert implements Command {
private final int at;
private final String text;
Insert(int at, String text) { this.at = at; this.text = text; }
public void execute(Doc doc) {
if (at < 0 || at > doc.text.length()) throw new IllegalArgumentException("insert outside the text");
doc.text = doc.text.substring(0, at) + text + doc.text.substring(at);
}
public void undo(Doc doc) {
doc.text = doc.text.substring(0, at) + doc.text.substring(at + text.length());
}
}
/** Needs stored state: what it removes is only known when it runs. */
static final class Delete implements Command {
private final int at, count;
private String removed = "";
Delete(int at, int count) { this.at = at; this.count = count; }
public void execute(Doc doc) {
if (at < 0 || at + count > doc.text.length()) throw new IllegalArgumentException("delete past the end");
removed = doc.text.substring(at, at + count);
doc.text = doc.text.substring(0, at) + doc.text.substring(at + count);
}
public void undo(Doc doc) {
doc.text = doc.text.substring(0, at) + removed + doc.text.substring(at);
}
}
/** No formula turns "BUY" back into "Buy", so it stores the text it replaced. */
static final class Uppercase implements Command {
private String before = "";
public void execute(Doc doc) {
before = doc.text;
doc.text = doc.text.toUpperCase();
}
public void undo(Doc doc) { doc.text = before; }
}
/** Several commands that undo as one step: run in order, undo in reverse. All or nothing. */
static final class Macro implements Command {
private final List<Command> commands;
Macro(List<Command> commands) { this.commands = commands; }
public void execute(Doc doc) {
List<Command> done = new ArrayList<>();
try {
for (Command c : commands) {
c.execute(doc);
done.add(c);
}
} catch (RuntimeException err) {
for (int i = done.size() - 1; i >= 0; i--) done.get(i).undo(doc); // roll back what already ran
throw err;
}
}
public void undo(Doc doc) {
for (int i = commands.size() - 1; i >= 0; i--) commands.get(i).undo(doc);
}
}
/** The invoker: it runs commands and keeps the undo and redo stacks. */
static final class Editor {
final Doc doc = new Doc();
private final int limit;
private final List<Command> undoStack = new ArrayList<>();
private final List<Command> redoStack = new ArrayList<>();
Editor(int limit) { this.limit = limit; }
void run(Command cmd) {
cmd.execute(doc); // if this throws, nothing is recorded
undoStack.add(cmd);
redoStack.clear(); // a new edit ends the redo history
if (undoStack.size() > limit) undoStack.remove(0); // the oldest step falls off
}
boolean undo() {
if (undoStack.isEmpty()) return false;
Command cmd = undoStack.remove(undoStack.size() - 1);
cmd.undo(doc);
redoStack.add(cmd);
return true;
}
boolean redo() {
if (redoStack.isEmpty()) return false;
Command cmd = redoStack.remove(redoStack.size() - 1);
cmd.execute(doc);
undoStack.add(cmd);
return true;
}
}
static void show(String step, Editor ed) {
System.out.println(step + ": '" + ed.doc.text + "'");
}
public static void main(String[] args) {
Editor ed = new Editor(100);
ed.run(new Insert(0, "Buy milk"));
show("insert 'Buy milk'", ed);
ed.run(new Insert(8, " and eggs"));
show("insert ' and eggs'", ed);
ed.run(new Delete(4, 5));
show("delete 'milk '", ed);
ed.run(new Uppercase());
show("uppercase", ed);
ed.undo();
show("undo", ed);
ed.undo();
show("undo", ed);
ed.redo();
show("redo", ed);
ed.run(new Macro(List.of(new Delete(4, 3), new Insert(4, "&"))));
show("macro: replace 'and' with '&'", ed);
System.out.println("redo: " + (ed.redo() ? "done" : "nothing to redo"));
for (Command bad : List.of(new Delete(40, 2), new Macro(List.of(new Insert(0, "X"), new Delete(99, 1))))) {
try {
ed.run(bad);
} catch (IllegalArgumentException err) {
System.out.println("rejected (" + err.getMessage() + "): '" + ed.doc.text + "'");
}
}
ed.undo();
show("undo", ed);
Editor small = new Editor(2);
String letters = "abc";
for (int i = 0; i < letters.length(); i++) small.run(new Insert(i, String.valueOf(letters.charAt(i))));
System.out.println("-- history limit 2: '" + small.doc.text + "'");
for (int i = 0; i < 3; i++) {
boolean ok = small.undo();
System.out.println("undo: " + (ok ? "'" + small.doc.text + "'" : "nothing to undo"));
}
}
}// notes_commands.cpp: every edit is an object that can do itself and undo itself
#include <cctype>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
// The receiver: the thing the commands change.
struct Doc {
std::string text;
};
class Command {
public:
virtual ~Command() = default;
virtual void execute(Doc& doc) = 0;
virtual void undo(Doc& doc) = 0;
};
// Its own data is enough to reverse it: remove what it added.
class Insert : public Command {
int at_;
std::string text_;
public:
Insert(int at, std::string text) : at_(at), text_(std::move(text)) {}
void execute(Doc& doc) override {
if (at_ < 0 || at_ > static_cast<int>(doc.text.size())) throw std::invalid_argument("insert outside the text");
doc.text.insert(at_, text_);
}
void undo(Doc& doc) override { doc.text.erase(at_, text_.size()); }
};
// Needs stored state: what it removes is only known when it runs.
class Delete : public Command {
int at_, count_;
std::string removed_;
public:
Delete(int at, int count) : at_(at), count_(count) {}
void execute(Doc& doc) override {
if (at_ < 0 || at_ + count_ > static_cast<int>(doc.text.size())) throw std::invalid_argument("delete past the end");
removed_ = doc.text.substr(at_, count_);
doc.text.erase(at_, count_);
}
void undo(Doc& doc) override { doc.text.insert(at_, removed_); }
};
// No formula turns "BUY" back into "Buy", so it stores the text it replaced.
class Uppercase : public Command {
std::string before_;
public:
void execute(Doc& doc) override {
before_ = doc.text;
for (char& c : doc.text) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
void undo(Doc& doc) override { doc.text = before_; }
};
// Several commands that undo as one step: run in order, undo in reverse. All or nothing.
class Macro : public Command {
std::vector<std::unique_ptr<Command>> commands_;
public:
explicit Macro(std::vector<std::unique_ptr<Command>> commands) : commands_(std::move(commands)) {}
void execute(Doc& doc) override {
size_t done = 0;
try {
for (auto& c : commands_) {
c->execute(doc);
++done;
}
} catch (...) {
for (size_t i = done; i-- > 0;) commands_[i]->undo(doc); // roll back what already ran
throw;
}
}
void undo(Doc& doc) override {
for (size_t i = commands_.size(); i-- > 0;) commands_[i]->undo(doc);
}
};
template <typename... Cs>
std::unique_ptr<Command> macro(std::unique_ptr<Cs>... cs) {
std::vector<std::unique_ptr<Command>> steps;
(steps.push_back(std::move(cs)), ...);
return std::make_unique<Macro>(std::move(steps));
}
// The invoker: it runs commands and keeps the undo and redo stacks.
class Editor {
size_t limit_;
std::vector<std::unique_ptr<Command>> undo_, redo_;
public:
Doc doc;
explicit Editor(size_t limit = 100) : limit_(limit) {}
void run(std::unique_ptr<Command> cmd) {
cmd->execute(doc); // if this throws, nothing is recorded
undo_.push_back(std::move(cmd));
redo_.clear(); // a new edit ends the redo history
if (undo_.size() > limit_) undo_.erase(undo_.begin()); // the oldest step falls off
}
bool undo() {
if (undo_.empty()) return false;
auto cmd = std::move(undo_.back());
undo_.pop_back();
cmd->undo(doc);
redo_.push_back(std::move(cmd));
return true;
}
bool redo() {
if (redo_.empty()) return false;
auto cmd = std::move(redo_.back());
redo_.pop_back();
cmd->execute(doc);
undo_.push_back(std::move(cmd));
return true;
}
};
void show(const std::string& step, const Editor& ed) {
std::cout << step << ": '" << ed.doc.text << "'\n";
}
int main() {
Editor ed;
ed.run(std::make_unique<Insert>(0, "Buy milk"));
show("insert 'Buy milk'", ed);
ed.run(std::make_unique<Insert>(8, " and eggs"));
show("insert ' and eggs'", ed);
ed.run(std::make_unique<Delete>(4, 5));
show("delete 'milk '", ed);
ed.run(std::make_unique<Uppercase>());
show("uppercase", ed);
ed.undo();
show("undo", ed);
ed.undo();
show("undo", ed);
ed.redo();
show("redo", ed);
ed.run(macro(std::make_unique<Delete>(4, 3), std::make_unique<Insert>(4, "&")));
show("macro: replace 'and' with '&'", ed);
std::cout << "redo: " << (ed.redo() ? "done" : "nothing to redo") << "\n";
std::vector<std::unique_ptr<Command>> bad;
bad.push_back(std::make_unique<Delete>(40, 2));
bad.push_back(macro(std::make_unique<Insert>(0, "X"), std::make_unique<Delete>(99, 1)));
for (auto& b : bad) {
try {
ed.run(std::move(b));
} catch (const std::invalid_argument& err) {
std::cout << "rejected (" << err.what() << "): '" << ed.doc.text << "'\n";
}
}
ed.undo();
show("undo", ed);
Editor small(2);
std::string letters = "abc";
for (size_t i = 0; i < letters.size(); i++) small.run(std::make_unique<Insert>(static_cast<int>(i), std::string(1, letters[i])));
std::cout << "-- history limit 2: '" << small.doc.text << "'\n";
for (int i = 0; i < 3; i++) {
bool ok = small.undo();
std::cout << "undo: " << (ok ? "'" + small.doc.text + "'" : std::string("nothing to undo")) << "\n";
}
}// notesCommands.js: every edit is an object that can do itself and undo itself
/** The receiver: the thing the commands change. */
class Doc {
constructor() {
this.text = "";
}
}
// The command contract: execute(doc) and undo(doc). JavaScript has no interfaces, so a test per command checks it.
/** Its own data is enough to reverse it: remove what it added. */
class Insert {
constructor(at, text) {
Object.assign(this, { at, text });
}
execute(doc) {
if (this.at < 0 || this.at > doc.text.length) throw new Error("insert outside the text");
doc.text = doc.text.slice(0, this.at) + this.text + doc.text.slice(this.at);
}
undo(doc) {
doc.text = doc.text.slice(0, this.at) + doc.text.slice(this.at + this.text.length);
}
}
/** Needs stored state: what it removes is only known when it runs. */
class Delete {
constructor(at, count) {
Object.assign(this, { at, count, removed: "" });
}
execute(doc) {
if (this.at < 0 || this.at + this.count > doc.text.length) throw new Error("delete past the end");
this.removed = doc.text.slice(this.at, this.at + this.count);
doc.text = doc.text.slice(0, this.at) + doc.text.slice(this.at + this.count);
}
undo(doc) {
doc.text = doc.text.slice(0, this.at) + this.removed + doc.text.slice(this.at);
}
}
/** No formula turns "BUY" back into "Buy", so it stores the text it replaced. */
class Uppercase {
constructor() {
this.before = "";
}
execute(doc) {
this.before = doc.text;
doc.text = doc.text.toUpperCase();
}
undo(doc) {
doc.text = this.before;
}
}
/** Several commands that undo as one step: run in order, undo in reverse. All or nothing. */
class Macro {
constructor(commands) {
this.commands = commands;
}
execute(doc) {
const done = [];
try {
for (const c of this.commands) {
c.execute(doc);
done.push(c);
}
} catch (err) {
for (const c of done.reverse()) c.undo(doc); // roll back what already ran
throw err;
}
}
undo(doc) {
for (const c of [...this.commands].reverse()) c.undo(doc);
}
}
/** The invoker: it runs commands and keeps the undo and redo stacks. */
class Editor {
constructor(limit = 100) {
this.doc = new Doc();
this.limit = limit;
this.undoStack = [];
this.redoStack = [];
}
run(cmd) {
cmd.execute(this.doc); // if this throws, nothing is recorded
this.undoStack.push(cmd);
this.redoStack.length = 0; // a new edit ends the redo history
if (this.undoStack.length > this.limit) this.undoStack.shift(); // the oldest step falls off
}
undo() {
if (this.undoStack.length === 0) return false;
const cmd = this.undoStack.pop();
cmd.undo(this.doc);
this.redoStack.push(cmd);
return true;
}
redo() {
if (this.redoStack.length === 0) return false;
const cmd = this.redoStack.pop();
cmd.execute(this.doc);
this.undoStack.push(cmd);
return true;
}
}
const show = (step, ed) => console.log(`${step}: '${ed.doc.text}'`);
const ed = new Editor();
ed.run(new Insert(0, "Buy milk"));
show("insert 'Buy milk'", ed);
ed.run(new Insert(8, " and eggs"));
show("insert ' and eggs'", ed);
ed.run(new Delete(4, 5));
show("delete 'milk '", ed);
ed.run(new Uppercase());
show("uppercase", ed);
ed.undo();
show("undo", ed);
ed.undo();
show("undo", ed);
ed.redo();
show("redo", ed);
ed.run(new Macro([new Delete(4, 3), new Insert(4, "&")]));
show("macro: replace 'and' with '&'", ed);
console.log("redo:", ed.redo() ? "done" : "nothing to redo");
for (const bad of [new Delete(40, 2), new Macro([new Insert(0, "X"), new Delete(99, 1)])]) {
try {
ed.run(bad);
} catch (err) {
console.log(`rejected (${err.message}): '${ed.doc.text}'`);
}
}
ed.undo();
show("undo", ed);
const small = new Editor(2);
[..."abc"].forEach((ch, i) => small.run(new Insert(i, ch)));
console.log(`-- history limit 2: '${small.doc.text}'`);
for (let i = 0; i < 3; i++) {
const ok = small.undo();
console.log("undo:", ok ? `'${small.doc.text}'` : "nothing to undo");
}All four print the same thing:
insert 'Buy milk': 'Buy milk'
insert ' and eggs': 'Buy milk and eggs'
delete 'milk ': 'Buy and eggs'
uppercase: 'BUY AND EGGS'
undo: 'Buy and eggs'
undo: 'Buy milk and eggs'
redo: 'Buy and eggs'
macro: replace 'and' with '&': 'Buy & eggs'
redo: nothing to redo
rejected (delete past the end): 'Buy & eggs'
rejected (delete past the end): 'Buy & eggs'
undo: 'Buy and eggs'
-- history limit 2: 'abc'
undo: 'ab'
undo: 'a'
undo: nothing to undoCheck a few lines by hand. Delete(4, 5) takes five characters from position 4 of Buy milk and eggs: the word milk and the space after it, leaving Buy and eggs. The two undos run Uppercase.undo and then Delete.undo, so the text goes back to Buy milk and eggs, and redo runs the delete again. After the macro, redo has nothing to do: running a new command cleared the redo stack. Both rejected commands changed nothing, and that is why the final undo reverses the macro (one step for two edits) and not a half-applied edit. With a history of 2, the first of three inserts has dropped off the bottom, so the third undo finds nothing and the text stays a.
What each language brings to the same design:
- Python: an abstract base class for
Command, and a plain list as each stack. A command is any object with the two methods, so a closure pair could do for the simplest cases. - Java: an
interfacewith two methods. A record could hold a command that has no stored state;DeleteandUppercaseneed mutable fields, so they are classes. - C++: a virtual base class with a virtual destructor. The stacks own their commands through
std::unique_ptr, and moving a command from one stack to the other moves the ownership with it. - JavaScript: no interfaces, so the contract is the two method names. Keep a test per command that runs it and undoes it, because nothing else checks the spelling.
What undo needs: an inverse, or stored state#
There are two ways for a command to reverse itself, and each of the three edits above shows one.
Its own data is enough: an inverse
Insert(at, text)undoes by removinglen(text)characters atat- Nothing extra is stored
- Works when the change can be undone by a formula
It must remember: stored state
Deletestores the text it removedUppercasestores the text it replaced, because no formula turnsBUYback intoBuy- Capture it in
execute, not in the constructor
The last line matters. A Delete is built before it runs, and the text at that position isn't known until it does (it can change between the moment a button is pressed and the moment the command runs). So execute takes what it needs, and redo, which is just execute again, takes it again.
The alternative to storing the change is storing the whole document before every edit and restoring it on undo (the Memento pattern, dp-23). It is simpler, and it can't get an inverse wrong. It is also the expensive way to keep a long history: the text editor lesson measures it, and keeping the last 100 edits of a 50,000-character document, commands hold 100 characters where snapshots hold 5,014,950. Snapshots suit small state or rare checkpoints such as autosave; commands suit keystroke-level undo.
The rules that make the two stacks behave like a real editor's are small, and each is in the code above:
- A new command clears the redo stack. Once you edit after an undo, the path you came back from no longer exists, as in every editor.
- A command that fails is not recorded.
executeruns first; only if it returns does the editor push it. Both rejected commands in the demo left no trace. - History is bounded. Without a limit the undo stack grows for ever; with one, the oldest step falls off the bottom.
- A macro undoes in reverse, and is all or nothing. Undoing the delete before the insert would put
andback in front of the&, and the insert's undo would then remove the wrong character. If the second step of a macro fails, the first is rolled back, so a macro never leaves a half-done edit.
Edit, undo, edit
You type A, type B, press undo (the text is A), then type C. How many undo steps does the editor offer now, and can you redo B?
One strong answer
Two steps, and no. The undo stack holds A and C. Running C cleared the redo stack, so B is gone for good. That is the rule of every editor, and it is what run does: execute, push, clear redo.
Undo and redo are two stacks: run pushes on undo, undo moves a command to redo, and any new command clears the redo stack.
Queue, log and retry: a payout job#
Undo is one use of a request-as-object. The others the intent names are queue and log. A payout is a good example, because a payout is a request that will be run later, by a worker, and may need to be run again.
The command here is plain data: an id, an account and an amount, plus execute(bank). The queue holds commands, logs every step, retries a failure up to three times, and parks a payout that never works. One payout (p2) is set up for the dangerous failure: the bank applies it and then the reply is lost.
# payout_queue.py: a request as an object can wait in a queue, be logged, and be retried
from collections import deque
from dataclasses import dataclass
from typing import Deque, Dict, List, Set
class Bank:
"""The receiver. A payout with the same id is applied once, however often it is sent."""
def __init__(self, accounts: List[str]) -> None:
self.balances: Dict[str, int] = {a: 0 for a in accounts}
self.applied: Set[str] = set()
self.timeouts_after_paying: Dict[str, int] = {"p2": 1} # pays, then the reply is lost
self.always_down: Set[str] = {"p3"} # never gets through
def pay(self, payout_id: str, account: str, amount: int) -> None:
if payout_id in self.always_down:
raise ConnectionError("bank down")
if payout_id not in self.applied:
self.balances[account] += amount
self.applied.add(payout_id)
if self.timeouts_after_paying.get(payout_id, 0) > 0:
self.timeouts_after_paying[payout_id] -= 1
raise TimeoutError("bank timed out")
@dataclass(frozen=True)
class Payout:
"""The command: plain data (an id, an account, an amount) plus execute."""
payout_id: str
account: str
amount: int
def execute(self, bank: Bank) -> None:
bank.pay(self.payout_id, self.account, self.amount)
class PayoutQueue:
"""The invoker: it queues commands, logs every step, retries, and parks the ones that never work."""
def __init__(self, bank: Bank, max_tries: int = 3) -> None:
self.bank, self.max_tries = bank, max_tries
self.queue: Deque[Payout] = deque()
self.log: List[str] = []
self.dead: List[str] = []
def submit(self, cmd: Payout) -> None:
self.queue.append(cmd)
self.log.append(f"queued {cmd.payout_id}")
def run_all(self) -> None:
while self.queue:
cmd = self.queue.popleft()
for attempt in range(1, self.max_tries + 1):
self.log.append(f"try {cmd.payout_id} ({attempt}/{self.max_tries})")
try:
cmd.execute(self.bank)
self.log.append(f"done {cmd.payout_id}")
break
except (TimeoutError, ConnectionError) as err:
if attempt == self.max_tries:
self.log.append(f"failed {cmd.payout_id} after {attempt} tries: {err}")
self.dead.append(cmd.payout_id)
else:
self.log.append(f"retry {cmd.payout_id}: {err}")
bank = Bank(["asha", "ravi", "meera"])
payouts = PayoutQueue(bank)
payouts.submit(Payout("p1", "asha", 500))
payouts.submit(Payout("p2", "ravi", 300))
payouts.submit(Payout("p3", "meera", 200))
payouts.run_all()
print("\n".join(payouts.log))
print("balances:", ", ".join(f"{a} {bank.balances[a]}" for a in sorted(bank.balances)))
print("dead letters:", ", ".join(payouts.dead) or "none")// PayoutQueue.java: a request as an object can wait in a queue, be logged, and be retried
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
class PayoutQueueDemo {
static final class BankError extends RuntimeException { // a failure worth retrying
BankError(String message) { super(message); }
}
/** The receiver. A payout with the same id is applied once, however often it is sent. */
static final class Bank {
final Map<String, Integer> balances = new TreeMap<>();
private final Set<String> applied = new HashSet<>();
private final Map<String, Integer> timeoutsAfterPaying = new HashMap<>(Map.of("p2", 1)); // pays, then the reply is lost
private final Set<String> alwaysDown = Set.of("p3"); // never gets through
Bank(List<String> accounts) { for (String a : accounts) balances.put(a, 0); }
void pay(String payoutId, String account, int amount) {
if (alwaysDown.contains(payoutId)) throw new BankError("bank down");
if (!applied.contains(payoutId)) {
balances.merge(account, amount, Integer::sum);
applied.add(payoutId);
}
if (timeoutsAfterPaying.getOrDefault(payoutId, 0) > 0) {
timeoutsAfterPaying.merge(payoutId, -1, Integer::sum);
throw new BankError("bank timed out");
}
}
}
/** The command: plain data (an id, an account, an amount) plus execute. */
record Payout(String payoutId, String account, int amount) {
void execute(Bank bank) { bank.pay(payoutId, account, amount); }
}
/** The invoker: it queues commands, logs every step, retries, and parks the ones that never work. */
static final class PayoutQueue {
private final Bank bank;
private final int maxTries;
private final Deque<Payout> queue = new ArrayDeque<>();
final List<String> log = new ArrayList<>();
final List<String> dead = new ArrayList<>();
PayoutQueue(Bank bank, int maxTries) { this.bank = bank; this.maxTries = maxTries; }
void submit(Payout cmd) {
queue.addLast(cmd);
log.add("queued " + cmd.payoutId());
}
void runAll() {
while (!queue.isEmpty()) {
Payout cmd = queue.removeFirst();
for (int attempt = 1; attempt <= maxTries; attempt++) {
log.add("try " + cmd.payoutId() + " (" + attempt + "/" + maxTries + ")");
try {
cmd.execute(bank);
log.add("done " + cmd.payoutId());
break;
} catch (BankError err) {
if (attempt == maxTries) {
log.add("failed " + cmd.payoutId() + " after " + attempt + " tries: " + err.getMessage());
dead.add(cmd.payoutId());
} else {
log.add("retry " + cmd.payoutId() + ": " + err.getMessage());
}
}
}
}
}
}
public static void main(String[] args) {
Bank bank = new Bank(List.of("asha", "ravi", "meera"));
PayoutQueue payouts = new PayoutQueue(bank, 3);
payouts.submit(new Payout("p1", "asha", 500));
payouts.submit(new Payout("p2", "ravi", 300));
payouts.submit(new Payout("p3", "meera", 200));
payouts.runAll();
System.out.println(String.join("\n", payouts.log));
List<String> parts = new ArrayList<>();
bank.balances.forEach((account, balance) -> parts.add(account + " " + balance));
System.out.println("balances: " + String.join(", ", parts));
System.out.println("dead letters: " + (payouts.dead.isEmpty() ? "none" : String.join(", ", payouts.dead)));
}
}// payout_queue.cpp: a request as an object can wait in a queue, be logged, and be retried
#include <deque>
#include <iostream>
#include <map>
#include <set>
#include <stdexcept>
#include <string>
#include <vector>
struct BankError : std::runtime_error { // a failure worth retrying
using std::runtime_error::runtime_error;
};
// The receiver. A payout with the same id is applied once, however often it is sent.
class Bank {
std::set<std::string> applied_;
std::map<std::string, int> timeoutsAfterPaying_{{"p2", 1}}; // pays, then the reply is lost
std::set<std::string> alwaysDown_{"p3"}; // never gets through
public:
std::map<std::string, int> balances;
explicit Bank(const std::vector<std::string>& accounts) {
for (const auto& a : accounts) balances[a] = 0;
}
void pay(const std::string& payoutId, const std::string& account, int amount) {
if (alwaysDown_.count(payoutId)) throw BankError("bank down");
if (!applied_.count(payoutId)) {
balances[account] += amount;
applied_.insert(payoutId);
}
if (timeoutsAfterPaying_[payoutId] > 0) {
timeoutsAfterPaying_[payoutId] -= 1;
throw BankError("bank timed out");
}
}
};
// The command: plain data (an id, an account, an amount) plus execute.
struct Payout {
std::string payoutId, account;
int amount;
void execute(Bank& bank) const { bank.pay(payoutId, account, amount); }
};
// The invoker: it queues commands, logs every step, retries, and parks the ones that never work.
class PayoutQueue {
Bank& bank_;
int maxTries_;
std::deque<Payout> queue_;
public:
std::vector<std::string> log, dead;
PayoutQueue(Bank& bank, int maxTries) : bank_(bank), maxTries_(maxTries) {}
void submit(const Payout& cmd) {
queue_.push_back(cmd);
log.push_back("queued " + cmd.payoutId);
}
void runAll() {
while (!queue_.empty()) {
Payout cmd = queue_.front();
queue_.pop_front();
for (int attempt = 1; attempt <= maxTries_; attempt++) {
log.push_back("try " + cmd.payoutId + " (" + std::to_string(attempt) + "/" + std::to_string(maxTries_) + ")");
try {
cmd.execute(bank_);
log.push_back("done " + cmd.payoutId);
break;
} catch (const BankError& err) {
if (attempt == maxTries_) {
log.push_back("failed " + cmd.payoutId + " after " + std::to_string(attempt) + " tries: " + err.what());
dead.push_back(cmd.payoutId);
} else {
log.push_back("retry " + cmd.payoutId + ": " + err.what());
}
}
}
}
}
};
int main() {
Bank bank({"asha", "ravi", "meera"});
PayoutQueue payouts(bank, 3);
payouts.submit({"p1", "asha", 500});
payouts.submit({"p2", "ravi", 300});
payouts.submit({"p3", "meera", 200});
payouts.runAll();
for (const auto& line : payouts.log) std::cout << line << "\n";
std::string balances;
for (const auto& entry : bank.balances) balances += (balances.empty() ? "" : ", ") + entry.first + " " + std::to_string(entry.second);
std::cout << "balances: " << balances << "\n";
std::string dead;
for (const auto& id : payouts.dead) dead += (dead.empty() ? "" : ", ") + id;
std::cout << "dead letters: " << (dead.empty() ? "none" : dead) << "\n";
}// payoutQueue.js: a request as an object can wait in a queue, be logged, and be retried
class BankError extends Error {} // a failure worth retrying
/** The receiver. A payout with the same id is applied once, however often it is sent. */
class Bank {
constructor(accounts) {
this.balances = new Map(accounts.map((a) => [a, 0]));
this.applied = new Set();
this.timeoutsAfterPaying = new Map([["p2", 1]]); // pays, then the reply is lost
this.alwaysDown = new Set(["p3"]); // never gets through
}
pay(payoutId, account, amount) {
if (this.alwaysDown.has(payoutId)) throw new BankError("bank down");
if (!this.applied.has(payoutId)) {
this.balances.set(account, this.balances.get(account) + amount);
this.applied.add(payoutId);
}
if ((this.timeoutsAfterPaying.get(payoutId) ?? 0) > 0) {
this.timeoutsAfterPaying.set(payoutId, this.timeoutsAfterPaying.get(payoutId) - 1);
throw new BankError("bank timed out");
}
}
}
/** The command: plain data (an id, an account, an amount) plus execute. */
class Payout {
constructor(payoutId, account, amount) {
Object.assign(this, { payoutId, account, amount });
}
execute(bank) {
bank.pay(this.payoutId, this.account, this.amount);
}
}
/** The invoker: it queues commands, logs every step, retries, and parks the ones that never work. */
class PayoutQueue {
constructor(bank, maxTries = 3) {
this.bank = bank;
this.maxTries = maxTries;
this.queue = [];
this.log = [];
this.dead = [];
}
submit(cmd) {
this.queue.push(cmd);
this.log.push(`queued ${cmd.payoutId}`);
}
runAll() {
while (this.queue.length > 0) {
const cmd = this.queue.shift();
for (let attempt = 1; attempt <= this.maxTries; attempt++) {
this.log.push(`try ${cmd.payoutId} (${attempt}/${this.maxTries})`);
try {
cmd.execute(this.bank);
this.log.push(`done ${cmd.payoutId}`);
break;
} catch (err) {
if (!(err instanceof BankError)) throw err;
if (attempt === this.maxTries) {
this.log.push(`failed ${cmd.payoutId} after ${attempt} tries: ${err.message}`);
this.dead.push(cmd.payoutId);
} else {
this.log.push(`retry ${cmd.payoutId}: ${err.message}`);
}
}
}
}
}
}
const bank = new Bank(["asha", "ravi", "meera"]);
const payouts = new PayoutQueue(bank);
payouts.submit(new Payout("p1", "asha", 500));
payouts.submit(new Payout("p2", "ravi", 300));
payouts.submit(new Payout("p3", "meera", 200));
payouts.runAll();
console.log(payouts.log.join("\n"));
console.log("balances:", [...bank.balances.keys()].sort().map((a) => `${a} ${bank.balances.get(a)}`).join(", "));
console.log("dead letters:", payouts.dead.join(", ") || "none");queued p1
queued p2
queued p3
try p1 (1/3)
done p1
try p2 (1/3)
retry p2: bank timed out
try p2 (2/3)
done p2
try p3 (1/3)
retry p3: bank down
try p3 (2/3)
retry p3: bank down
try p3 (3/3)
failed p3 after 3 tries: bank down
balances: asha 500, meera 0, ravi 300
dead letters: p3Read the log. p2 was tried twice, and Ravi has 300, not 600. The bank paid on the first try and then timed out, so the queue saw a failure for something that had happened. The second try carried the same id, and the bank, which remembers the ids it has applied, did nothing and answered success. That is the property a retried command needs: the receiver must recognise a repeat. The id is part of the command for this reason, and the course's lesson on idempotency builds it properly. Without it, a retry that is safe for a failure before the payment pays twice for a failure after it.
p3 never gets through. After three tries it is logged as failed and parked in a list of dead letters, where a person can see it, instead of retried for ever or dropped (dead letters in Week 3). Three things in this design come from the command being data: it can wait in a queue, a log line can name it, and the same object can be written down (its id, account and amount) and replayed after a crash. That is the order book's answer to "what if the process dies?": log every command before matching it, and replay the log.
One more difference from the notes app. A payout has no undo: money that has left can't be taken back by running the code backwards. The way to reverse it is a new command, a refund (a compensating action), which goes through the same queue with its own id. Undo by inverse works for state you own and can restore; for effects in the outside world, you compensate.
A retry with no id can pay twice: the bank paid, the reply was lost, and the queue can't tell. An id lets the receiver ignore the repeat.
Command and its look-alikes#
| Pattern | Looks like Command because | The difference |
|---|---|---|
| Strategy (dp-16) | one object with one main method | A strategy is how to do one job, chosen and kept by the context. A command is a request, with its arguments, made to be run later, queued or undone. |
| A callback or closure | code is passed around to run later | A function is a command without undo, a name or a record of its arguments. It's enough to run something later; use the class form when you need to undo, log or replay. |
| Memento (dp-23) | both give you undo | A memento stores a snapshot of the state; a command stores the change. |
| An event (Observer) | a small object describing something | An event says something happened, in the past tense, to anyone listening. A command says do this, to one receiver, and may be refused. |
| Composite (dp-13) | a tree of the same interface | Macro is a composite of commands: it is a command made of commands. |
When not to use it#
A command is a class with a constructor and two methods for what could have been a method call. If nothing will ever queue, log, undo or retry the request, that is a cost with no return.
- It's a plain call that runs now and is never undone.
print_receipt(order)doesn't need to be an object. - The state is small and rarely undone. Saving a copy of the state is simpler than writing an inverse for every action, and can't get one wrong.
- The effect can't be reversed. An email that has been sent has no
undo. Model the apology as another command, or don't pretend it's undoable. - A single function is passed to run later. A closure is a command without ceremony; promote it to a class when you need undo or a log.
Where it shows up in the case studies#
Four of the 22 rounds use Command, in three different roles:
| Case study | The command | What it's used for |
|---|---|---|
| Elevator | HallCall (light) |
A request as a value object that can be queued, deduplicated, logged and replayed |
| Chess | Move + Undo, with push() and pop() |
An exact inverse for every move, used by legality checks and the undo button alike |
| Text editor | Insert, Delete, Compound with do, undo and absorb |
Undo and redo stacks, a bounded history and keystrokes grouped into word-sized steps |
| Order book | the command queue, Exchange._inbox |
One matcher thread per symbol runs commands in order; logging the commands lets you replay them |
The text editor is the one to read next: it has everything in this lesson, plus grouping keystrokes into steps (typing Hello world in one go takes two undos, one per word).
In the interview#
Say the pattern with the reason, in one breath: "Each edit is a command with execute and undo; the editor keeps an undo stack and a redo stack, so a new kind of edit is a new class and the editor never changes. Delete stores the text it removed, because that can't be worked out later." Naming where the undo data comes from is what separates a candidate who has built one.
Then expect the follow-ups:
- "Typing is one letter at a time. Is every key an undo step?" No: merge consecutive keystrokes into one step (the text editor lesson's
absorb), and end a group on a pause, a cursor move or a word boundary. - "Redo after a new edit?" Cleared. The redo history belongs to the path that no longer exists.
- "Limit the memory." Bound the stack, and say that the oldest steps fall off; or store inverses instead of snapshots, which is orders of magnitude less.
- "What if a command fails halfway?" Record nothing, and for a macro roll back the steps that ran, so an edit is applied whole or not at all.
- "Retry a failed job safely." Give the command an id and make the receiver ignore an id it has already applied; park a job that keeps failing instead of retrying for ever.
- "Recover after a crash." Log each command before running it and replay the log; snapshots shorten the replay.
- "How is this different from Strategy?" A strategy is how one job is done, set from outside. A command is a request that waits, repeats or reverses.
Common mistakes#
- Capturing undo data in the constructor. What a
Deleteremoves isn't known until it runs. Capture it inexecute. - Undoing a macro in the wrong order. Reverse order, always.
- Forgetting to clear redo when a new command runs, so redo replays edits onto a document that has moved on.
- Recording a command that failed. Push it only after
executereturns. - An unbounded history. A 100-step limit is a requirement the interviewer will ask about.
- Doing the work in the invoker. If
Editorhas anifon the kind of command, the pattern is gone. - Retrying without an id. A repeated payout pays twice. The receiver must be able to tell a repeat.
- Calling an external effect undoable. Money sent and emails delivered are compensated by a new command.
Command
Q1/4 The notes app adds a "paste" edit. In the command design, what is the right change?
Answer & explanation
Answer: B
A new kind of edit is a new class behind the same two methods. Editor only runs commands and moves them between stacks, so it stays closed to new edits.
Q2/4 A Delete command removes "milk ". What must it keep so that it can undo itself?
Answer & explanation
Answer: C
The text isn't known until the command runs, and after it runs the document no longer contains it. Capture it in execute. Redo, which is execute again, captures it again.
Q3/4 You type A, type B, press undo, then type C. What does redo do?
Answer & explanation
Answer: C
A new command ends the redo history, because the path through B no longer exists. run executes, pushes on the undo stack and clears the redo stack.
Q4/4 A queued payout p2 is applied by the bank, but the reply times out, so the queue retries it. What stops a second payment?
Answer & explanation
Answer: C
The failure happened after the payment, so the queue can't tell it from a failure before. The command's id lets the receiver recognise the repeat and do nothing.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


