Command and Chain of Responsibility

Topics Covered

The Command Pattern

The Four Participants

A Text Editor Example

Why This Matters in Interviews

Undo and Redo with Commands

The Two-Stack Model

Implementation

Composite Commands (Macros)

State Capture vs. Operation-Based Undo

Practical Considerations

The Chain of Responsibility Pattern

Structure

Implementation

Key Design Decisions

Building Request Pipelines

How Middleware Works

Implementation

Short-Circuiting

Pipeline Ordering Matters

Command vs Chain vs Strategy

The Three Patterns Compared

Decision Framework

Transaction Logging with Commands

Practice Problems

Why do GUI frameworks let you bind the same action to a menu item, a toolbar button, a keyboard shortcut, and a right-click menu without duplicating any logic? The answer is the Command pattern: you wrap each action in an object that knows how to execute itself. The button does not know what it does. It just calls execute() on whatever command object it holds.

This is the core insight. Instead of writing if button == "bold" then boldSelectedText() inside every UI element, you create a BoldCommand object and hand it to any invoker that needs it. The invoker (button, menu, shortcut) is completely decoupled from the receiver (the text editor engine). You can swap, queue, log, and undo commands because they are objects, not function calls buried in event handlers.

Key Insight

The Command pattern turns actions into first-class objects. Once an action is an object, you can store it in a list, serialize it to disk, send it over a network, replay it, or undo it. This single shift from 'call a function' to 'create an object that calls a function' unlocks undo/redo, macro recording, transaction logging, and distributed command queues.

Command pattern showing execute and undo cycle between invoker, command object, and receiver

The Four Participants

Every Command pattern implementation has the same four roles:

  1. Command interface defines execute() and undo(). Every concrete command implements both.
  2. ConcreteCommand (e.g., InsertTextCommand, DeleteTextCommand) holds a reference to the receiver and the parameters needed to perform and reverse the action.
  3. Receiver is the object that does the actual work. A TextEditor knows how to insert text at a position. The command tells it what to do.
  4. Invoker triggers execution. A toolbar button, a keyboard shortcut handler, or an automated test. The invoker holds a command reference and calls execute(). It never knows or cares what the command does.

A Text Editor Example

Consider a text editor with insert, delete, and bold operations. Without commands, each button handler contains the logic directly:

python
1# Without Command pattern - logic scattered in handlers
2class ToolbarButton:
3    def on_click(self):
4        if self.action == "bold":
5            editor.bold_selection(start, end)
6        elif self.action == "insert":
7            editor.insert_text(position, text)
8        # Every new action means modifying this method

With the Command pattern, each action is a self-contained object:

python
1from abc import ABC, abstractmethod
2
3class Command(ABC):
4    @abstractmethod
5    def execute(self) -> None:
6        pass
7
8    @abstractmethod
9    def undo(self) -> None:
10        pass
11
12class InsertTextCommand(Command):
13    def __init__(self, editor, position: int, text: str):
14        self.editor = editor
15        self.position = position
16        self.text = text
17
18    def execute(self) -> None:
19        self.editor.insert(self.position, self.text)
20
21    def undo(self) -> None:
22        self.editor.delete(self.position, len(self.text))
23
24class BoldTextCommand(Command):
25    def __init__(self, editor, start: int, end: int):
26        self.editor = editor
27        self.start = start
28        self.end = end
29        self.was_bold = None
30
31    def execute(self) -> None:
32        self.was_bold = self.editor.is_bold(self.start, self.end)
33        self.editor.toggle_bold(self.start, self.end)
34
35    def undo(self) -> None:
36        if not self.was_bold:
37            self.editor.toggle_bold(self.start, self.end)

The invoker is completely generic:

python
1class ToolbarButton:
2    def __init__(self, command: Command):
3        self.command = command
4
5    def on_click(self):
6        self.command.execute()

Adding a new action (italic, underline, find-and-replace) means writing one new Command subclass. The toolbar, menu, and shortcut system never change.

Why This Matters in Interviews

Interviewers ask about the Command pattern when they want to see if you understand decoupling. The pattern separates what happens from who triggers it and when it runs. If your design has a giant switch statement routing actions to logic, the Command pattern is the fix.

The Command pattern earns its place in real codebases the moment you need undo and redo. Every command knows two things: how to do the action and how to reverse it. That symmetry between execute() and undo() is what makes the entire system work.

Undo and redo stack operations showing commands moving between the two stacks

The Two-Stack Model

The undo/redo mechanism uses two stacks:

  • Undo stack: Every time a command executes, it is pushed onto the undo stack.
  • Redo stack: When you undo a command, it is popped from the undo stack and pushed onto the redo stack.
  • Redo after undo: Popping from the redo stack re-executes the command and pushes it back onto the undo stack.
  • Critical rule: When a new command executes (not a redo), the redo stack is cleared. Once you undo three steps and then type a new character, those three undone commands are gone forever. This prevents a confusing branching history.

Implementation

python
1class UndoRedoManager:
2    def __init__(self):
3        self.undo_stack: list[Command] = []
4        self.redo_stack: list[Command] = []
5
6    def execute(self, command: Command) -> None:
7        command.execute()
8        self.undo_stack.append(command)
9        self.redo_stack.clear()  # New action kills redo history
10
11    def undo(self) -> None:
12        if not self.undo_stack:
13            return
14        command = self.undo_stack.pop()
15        command.undo()
16        self.redo_stack.append(command)
17
18    def redo(self) -> None:
19        if not self.redo_stack:
20            return
21        command = self.redo_stack.pop()
22        command.execute()
23        self.undo_stack.append(command)

Notice that redo() calls execute(), not a separate redo method. The command already knows how to do the action. Redo is just "do it again."

Composite Commands (Macros)

Sometimes a single user action involves multiple low-level operations. "Find and replace all" might execute 47 individual ReplaceCommand objects. The user expects Ctrl+Z to undo the entire find-and-replace, not each replacement one at a time.

A CompositeCommand (also called a macro command) groups multiple commands into one undo unit:

python
1class CompositeCommand(Command):
2    def __init__(self, commands: list[Command]):
3        self.commands = commands
4
5    def execute(self) -> None:
6        for cmd in self.commands:
7            cmd.execute()
8
9    def undo(self) -> None:
10        for cmd in reversed(self.commands):
11            cmd.undo()

The undo reverses commands in the opposite order. If you inserted text at position 10 and then deleted text at position 20, undoing the delete must happen before undoing the insert, because the insert changed the positions that the delete relied on.

State Capture vs. Operation-Based Undo

There are two approaches to undo:

Operation-based (Command pattern): Each command stores the minimum information needed to reverse itself. InsertTextCommand stores the position and length so it can delete. DeleteTextCommand stores the deleted text so it can re-insert.

State-based (Memento pattern): Take a full snapshot of the state before each action. Undo restores the snapshot. Simple but memory-expensive for large states.

The Command pattern uses operation-based undo. For a text editor with a 10MB document, storing "inserted 5 characters at position 200" is far cheaper than storing a 10MB snapshot before every keystroke.

Practical Considerations

  • Undo stack depth: Most applications cap the undo stack at 50-200 entries to limit memory usage.
  • Non-undoable commands: Some commands (save file, send email) cannot be undone. These execute but do not push onto the undo stack.
  • Command merging: Typing 10 characters one at a time produces 10 commands. Editors merge consecutive typing commands into a single "insert 'hello worl'" command to reduce undo noise.

The Command pattern solves "how do I encapsulate what to do." The Chain of Responsibility pattern solves a different problem: "who should handle this request when I don't know in advance?"

Think about tech support. You call with a problem. The L1 agent checks if it is a known issue with a scripted fix. If not, they escalate to L2 (experienced technician). If L2 cannot solve it, they escalate to L3 (specialist engineer). If L3 is stumped, it reaches the engineering manager. Each level either handles the request or passes it to the next. The caller does not choose which level handles the problem. The chain decides.

Interview Tip

In interviews, the Chain of Responsibility pattern comes up whenever a request might be handled by one of several objects and the sender should not decide which one. If you hear 'escalation,' 'fallback,' 'middleware,' or 'filter chain,' think Chain of Responsibility.

Chain of responsibility showing a request passing through a series of handlers until one processes it

Structure

The pattern has two participants:

  1. Handler interface defines handle(request) and set_next(handler). Each handler either processes the request or delegates to the next handler in the chain.
  2. ConcreteHandler implements the decision logic. It inspects the request, decides whether it can handle it, and either processes it or calls self.next_handler.handle(request).

Implementation

python
1from abc import ABC, abstractmethod
2from dataclasses import dataclass
3
4@dataclass
5class SupportTicket:
6    issue: str
7    severity: int  # 1 = low, 2 = medium, 3 = high, 4 = critical
8
9class SupportHandler(ABC):
10    def __init__(self):
11        self.next_handler: SupportHandler | None = None
12
13    def set_next(self, handler: 'SupportHandler') -> 'SupportHandler':
14        self.next_handler = handler
15        return handler  # Enables chaining: l1.set_next(l2).set_next(l3)
16
17    @abstractmethod
18    def handle(self, ticket: SupportTicket) -> str:
19        pass
20
21class L1Support(SupportHandler):
22    def handle(self, ticket: SupportTicket) -> str:
23        if ticket.severity == 1:
24            return f"L1 resolved: {ticket.issue}"
25        if self.next_handler:
26            return self.next_handler.handle(ticket)
27        return f"No handler for: {ticket.issue}"
28
29class L2Support(SupportHandler):
30    def handle(self, ticket: SupportTicket) -> str:
31        if ticket.severity <= 2:
32            return f"L2 resolved: {ticket.issue}"
33        if self.next_handler:
34            return self.next_handler.handle(ticket)
35        return f"No handler for: {ticket.issue}"
36
37class L3Support(SupportHandler):
38    def handle(self, ticket: SupportTicket) -> str:
39        if ticket.severity <= 3:
40            return f"L3 resolved: {ticket.issue}"
41        if self.next_handler:
42            return self.next_handler.handle(ticket)
43        return f"Escalated to management: {ticket.issue}"
44
45# Build the chain
46l1 = L1Support()
47l2 = L2Support()
48l3 = L3Support()
49l1.set_next(l2).set_next(l3)
50
51# Client sends to l1, does not know who handles it
52print(l1.handle(SupportTicket("Password reset", 1)))     # L1 resolves
53print(l1.handle(SupportTicket("Server crash", 3)))        # L3 resolves

Key Design Decisions

What if no handler processes the request? The chain must handle this gracefully. Options include: a default handler at the end that always accepts, returning a "not handled" response, or raising an exception. The worst choice is silently dropping the request.

Can a handler modify the request before passing it? Yes, and this is powerful. A logging handler might add a timestamp. An authentication handler might attach user identity. This transforms the chain from a "find the right handler" pattern into a "processing pipeline" pattern, which is the topic of the next section.

How do you order the chain? Typically from most specific to most general, or from cheapest check to most expensive. L1 support handles the easy cases quickly so L3 is not overwhelmed with password resets.

The Chain of Responsibility pattern has a powerful variant you use every day without realizing it: middleware pipelines. Every web framework (Express.js, Django, Flask, Spring) uses this pattern to process HTTP requests. The difference from the classic chain is that middleware does not stop at the first handler. Instead, every handler in the pipeline gets a chance to inspect, modify, or reject the request.

Middleware pipeline showing a request flowing through auth, rate limiting, logging, and route handler

How Middleware Works

A typical web request passes through a pipeline like this:

  1. Authentication middleware checks the token. Invalid token? Return 401 immediately. Valid? Attach the user identity to the request and pass it forward.
  2. Rate limiting middleware checks if this client has exceeded their quota. Exceeded? Return 429. Under limit? Increment the counter and pass forward.
  3. Logging middleware records the request method, path, and timestamp. Always passes forward.
  4. Route handler processes the actual business logic and returns a response.
  5. The response travels back through the pipeline. Logging middleware records the response time. Rate limiting updates its counters. Authentication does nothing on the way back.

Each middleware can do three things: modify the request (add headers, parse body), short-circuit the chain (return an error without calling the next handler), or modify the response on the way back.

Implementation

python
1from abc import ABC, abstractmethod
2from dataclasses import dataclass, field
3from typing import Callable
4
5@dataclass
6class Request:
7    path: str
8    method: str
9    headers: dict = field(default_factory=dict)
10    user: str | None = None
11
12@dataclass
13class Response:
14    status: int
15    body: str
16
17class Middleware(ABC):
18    def __init__(self):
19        self.next: Middleware | None = None
20
21    def set_next(self, middleware: 'Middleware') -> 'Middleware':
22        self.next = middleware
23        return middleware
24
25    @abstractmethod
26    def handle(self, request: Request) -> Response:
27        pass
28
29    def call_next(self, request: Request) -> Response:
30        if self.next:
31            return self.next.handle(request)
32        return Response(404, "Not Found")
33
34class AuthMiddleware(Middleware):
35    def __init__(self, valid_tokens: set):
36        super().__init__()
37        self.valid_tokens = valid_tokens
38
39    def handle(self, request: Request) -> Response:
40        token = request.headers.get("Authorization")
41        if token not in self.valid_tokens:
42            return Response(401, "Unauthorized")  # Short-circuit
43        request.user = token  # Modify request
44        return self.call_next(request)
45
46class RateLimitMiddleware(Middleware):
47    def __init__(self, max_requests: int):
48        super().__init__()
49        self.max_requests = max_requests
50        self.counts: dict[str, int] = {}
51
52    def handle(self, request: Request) -> Response:
53        client = request.user or "anonymous"
54        self.counts[client] = self.counts.get(client, 0) + 1
55        if self.counts[client] > self.max_requests:
56            return Response(429, "Rate limit exceeded")
57        return self.call_next(request)
58
59class LoggingMiddleware(Middleware):
60    def handle(self, request: Request) -> Response:
61        print(f"{request.method} {request.path} by {request.user}")
62        response = self.call_next(request)
63        print(f"Response: {response.status}")
64        return response
65
66# Build the pipeline
67auth = AuthMiddleware({"token-abc", "token-xyz"})
68rate_limit = RateLimitMiddleware(max_requests=100)
69logging_mw = LoggingMiddleware()
70auth.set_next(rate_limit).set_next(logging_mw)
71
72# Process a request
73req = Request("/api/users", "GET", {"Authorization": "token-abc"})
74resp = auth.handle(req)

Short-Circuiting

The most important middleware behavior is short-circuiting. When the auth middleware returns a 401, it never calls self.call_next(). The rate limiter, logger, and route handler never see the request. This is both a performance optimization (why parse the body if the token is invalid?) and a security boundary (unauthenticated requests never reach business logic).

Pipeline Ordering Matters

The order of middleware is a design decision with real consequences:

  • Auth before rate limiting: Rate limits apply per authenticated user, not per IP. More accurate but requires auth to run first.
  • Rate limiting before auth: Protects the auth service from brute-force attacks. A flood of invalid tokens hits the rate limiter before auth has to validate them.
  • Logging first: Captures all requests including rejected ones. Useful for debugging.
  • Logging last: Only captures successful requests. Cleaner logs but blind to failures.

There is no universally correct order. The right order depends on your threat model and observability requirements.

Interviews often ask you to compare behavioral patterns. Command, Chain of Responsibility, and Strategy all deal with "how to handle an action," but they answer different questions. Confusing them signals that you memorized pattern names without understanding the problems they solve.

Common Pitfall

A common interview mistake is saying 'Command and Strategy are basically the same because both encapsulate behavior in an object.' They are not. Command encapsulates a specific action with its parameters to be executed later. Strategy encapsulates an algorithm to be selected at runtime. Command is a noun (the action). Strategy is an adjective (the approach).

The Three Patterns Compared

Command answers: WHAT should happen?

A command object encapsulates a specific action (insert text, delete row, send email) along with all the parameters needed to execute and reverse it. The focus is on capturing the action as a self-contained, storable, undoable unit. Commands are typically executed once and kept for undo/logging.

Chain of Responsibility answers: WHO should handle this?

A request enters a chain of handlers. Each handler decides if it can process the request or should pass it along. The sender does not choose the handler. The chain's structure determines routing. The focus is on decoupling the sender from the receiver.

Strategy answers: HOW should this be done?

A strategy encapsulates an algorithm (sort with quicksort vs. mergesort, compress with gzip vs. zstd, price with flat-rate vs. tiered). The client selects the strategy based on context. The focus is on interchangeable algorithms behind a common interface.

Decision Framework

Ask yourself these questions when choosing a pattern:

  • Do you need to store, queue, undo, or replay an action? Use Command.
  • Do you need to route a request to one of several handlers without the sender knowing which? Use Chain of Responsibility.
  • Do you need to swap between different algorithms for the same task? Use Strategy.

Transaction Logging with Commands

One of the most powerful applications of the Command pattern is transaction logging. Every command that executes is serialized and appended to a log file. If the system crashes, you replay the log from the last checkpoint to recover the state.

This is exactly how databases implement write-ahead logging (WAL). Each SQL statement is a command. The WAL is the command log. Crash recovery replays the WAL. Event sourcing in distributed systems follows the same principle: store the commands (events), not the state. Derive the state by replaying commands.

Practice Problems

Apply these patterns to concrete implementations. The first problem combines Command with undo/redo stacks. The second builds a Chain of Responsibility middleware pipeline. The third uses Command for transaction logging with crash recovery.

Loading problem...

Loading problem...

Loading problem...