The Command Pattern is well known in object-oriented design, but how does it work across a network? In this article, we look at two approaches to implementing the Command Pattern in distributed systems, discuss their trade-offs, and examine how it integrates with the Saga pattern.
Let’s explore two possible implementations of the Command Patter over the network.
1. Client Commands
In this approach, a Command carries both the data and the actions to be executed on the server:
client server--------- ----------
| Cmd | | |
| ------- | -------> | Receiver |
| data | | |
| actions | | |
--------- ----------
At first glance, it might seem like a natural fit. After all, one of the Command Pattern’s key strengths is how easily you can introduce new functionality by simply adding new Commands, without modifying existing code.
However, embedding behavior in this way introduces several significant drawbacks:
- Without additional restrictions, a Command can perform actions on the server in an arbitrary order, which is generally unacceptable in most systems.
- The dynamic nature of Commands can lead to performance issues, especially when implemented with reflection.
- The complexity of this approach makes it difficult to implement.
2. Server Commands
A safer and more robust alternative is to move behavior to the server. In this case, the client sends a Command containing only a type identifier and data:
client server------------- ----------
| Cmd | | |
| ----------- | -------> | Receiver |
| type + data | | |
------------- ----------
The information about actions is located on the server side and is linked by deserializing data to the concrete Command type, like:
FooCmddata
Exec(r Receiver) {
// perform actions
}
This approach is significantly better:
- A server supports only a predefined set of Commands, so the client can’t interact with the Receiver in arbitrary ways. If the server receives a Command with an unrecognized type, it can simply close the connection.
- The overall design is easier to implement and maintain.
- As demonstrated by cmd-stream-go, this model performs extremely well (see benchmarks).
Rather than covering every detail of the Command pattern, this section highlights a few key aspects.
Transactional Behavior
The Command Pattern provides a powerful way to organize code. Each Command represents a single, complete unit of work that can either succeed or fail, while maintaining a clean and minimal interaction interface. Here’s a simple but descriptive example:
// Don’t take this code too seriously, it looks this way for the sake of// simplicity.
// Receiver exposes a minimal interface.
Receiver
Withdraw(...)
Deposit(...)
WithdrawCmd
Exec(r Receiver) {
// perform withdrawal
}
DepositCmd
Exec(r Receiver) {
// perform deposit
}
TransferCmd
Exec(r Receiver) {
// perform both withdrawal and deposit
}
Simple Commands execute specific actions, while composite Commands like TransferCmd can combine them to form higher-level operations.
Undo
What does undo actually mean? If a Command creates an order, does undo the Command means canceling the order? Not necessarily. Undo should eliminate all consequences of the Command’s execution, canceling the order might be just one part of that. We should be careful not to confuse business-level actions with true undo operations, which are meant to fully revert the system to its previous state.
Is the Command Pattern Suitable for Tracing?
Yes — and otelcmd-stream-go demonstrates this effectively. It adds OpenTelemetry instrumentation to cmd-stream-go, making it much easier to integrate tracing into your application (see more details here).
*A small reminder*: The Saga Pattern ensures data consistency across multiple services by coordinating a series of local transactions. If any transaction fails, previous changes should be undone through compensating actions.
The transactional behavior and undo capabilities of the Command Pattern fit naturally in this context. Each operation in a Saga can be encapsulated as a discrete, self-contained Command, capable of being executed, logged, and, if necessary, reversed. This approach not only simplifies orchestration but also provides a clear mechanism for implementing compensating actions.
The Command Pattern is a powerful and flexible tool for inter-service interaction. While it originates from object-oriented design, its benefits translate exceptionally well to networked environments. When combined with libraries like cmd-stream-go, this pattern not only promotes clean architecture but also delivers excellent performance and efficient resource utilization, resulting in lower infrastructure costs and happier users.
Whatever type of distributed system you’re designing, the Command Pattern offers a solid foundation to build on.