The Command Pattern in Go
Decoupling Actions from Execution for Scalable Systems

The Command Pattern is a design pattern that helps you turn actions or requests into standalone objects. Instead of calling functions directly, you wrap each action inside a “command,” which can then be passed around, stored, or executed later. This creates a clean separation between the part of your code that triggers an action and the part that actually performs it. In Go, this works especially well thanks to its simple interfaces and struct-based design, making it a neat way to organize and manage different operations in a flexible and scalable way.
After understanding what the Command Pattern is, the natural question is—why would you use it? In real-world applications, you often need more control over how and when actions are executed. For example, you might want to queue tasks, retry failed operations, log every action, or even support undo functionality. If you tightly couple function calls, these features become messy and hard to maintain. The Command Pattern solves this by treating each action as an object, making it easier to manage, extend, and compose behaviors without changing existing code.
Understanding the Command Pattern Through a Post Office Analogy
Think of a post office as a system that handles requests without the sender needing to know how the delivery actually happens. When you send a letter, you don’t directly hand it to the person who will deliver it—you give it to the post office. The letter acts as a packaged request containing all the necessary details (recipient, address, message). The post office (invoker) takes this request and ensures it reaches the right delivery agent (receiver), who then performs the actual task of delivering it. Similarly, in the Command Pattern, a request is wrapped inside a command object and passed around independently of the code that executes it. This separation allows the system to queue, track, or even delay requests without tightly coupling the sender and the executor.
Understanding the Different Parts of the Command Pattern
To see how the Command Pattern actually works under the hood, it helps to break it down into its core components. At the center is the Command, which defines a common interface (usually a single Execute method) that all concrete commands implement. Each Concrete Command wraps a specific action and holds a reference to the Receiver, which is the component that contains the actual business logic. The Invoker is responsible for triggering the command—it doesn’t know how the action is performed, it simply calls Execute. Finally, the Client ties everything together by creating command objects, assigning receivers, and passing them to the invoker. This separation of concerns is what makes the pattern powerful: each part has a single responsibility, and changes in one area don’t ripple across the entire system.
Implementing the Command Pattern: Restaurant Example
Let’s bring the Command Pattern to life using a restaurant scenario. When a customer places an order, the waiter doesn’t cook the food—they simply take the order and pass it to the kitchen. Here, the order acts as a command, the waiter is the invoker, and the chef in the kitchen is the receiver who actually prepares the dish. This setup neatly separates the request from its execution, allowing the system to handle multiple orders, queue them, or even modify them without affecting how they’re processed.
With this mental model in place, let’s jump into the code...
Step 1: Define the Command Interface
We start by defining a common interface that all commands will implement. In Go, this is typically just a single method like Execute().
type Command interface {
Execute()
}
This abstraction allows the invoker to trigger any command without knowing what it actually does.
Step 2: Create the Receiver (Chef)
The receiver contains the actual business logic. In our case, the chef knows how to prepare different dishes.
type Chef struct{}
func (c *Chef) CookPasta() {
println("Chef is cooking pasta 🍝")
}
func (c *Chef) CookPizza() {
println("Chef is cooking pizza 🍕")
}
The chef does the real work—but doesn’t know anything about commands or who requested the action.
Step 3: Create Concrete Commands (Orders)
Each order is a command that wraps a specific action and holds a reference to the receiver.
type PastaOrder struct {
chef *Chef
}
func (p *PastaOrder) Execute() {
p.chef.CookPasta()
}
type PizzaOrder struct {
chef *Chef
}
func (p *PizzaOrder) Execute() {
p.chef.CookPizza()
}
Each command translates a request into a call to the receiver.
Step 4: Create the Invoker (Waiter)
The waiter takes orders and triggers them, without knowing how they are executed.
type Waiter struct {
orders []Command
}
func (w *Waiter) TakeOrder(cmd Command) {
w.orders = append(w.orders, cmd)
}
func (w *Waiter) PlaceOrders() {
for _, order := range w.orders {
order.Execute()
}
}
The waiter simply stores and executes commands - completely decoupled from the actual cooking.
Step 5: Bring Everything Together (Client)
Now we wire everything up.
func main() {
chef := &Chef{}
// Create command objects (orders) with the receiver (chef)
pasta := &PastaOrder{chef: chef}
pizza := &PizzaOrder{chef: chef}
waiter := &Waiter{}
// Waiter takes orders (commands) without executing them immediately
// 👉 Benefit: Requests are decoupled from execution
waiter.TakeOrder(pasta)
waiter.TakeOrder(pizza)
// At this point, orders are stored and can be:
// 👉 queued
// 👉 logged
// 👉 modified before execution
// Execute all orders later
// 👉 Benefit: We control *when* execution happens
waiter.PlaceOrders()
// This design allows easy extension:
// 👉 Add new commands without changing existing code
// 👉 Support features like undo/redo, retries, or async processing
}
↩️ Adding Undo Functionality
To support undo, we need to slightly evolve our design. Instead of just an Execute() method, each command should also know how to reverse its action using an Undo() method.
Step 1: Update the Command Interface
type Command interface {
Execute()
Undo()
}
Now every command is responsible for both doing and undoing its action.
Step 2: Update the Receiver (Chef)
We need to introduce “reverse” actions.
type Chef struct{}
func (c *Chef) CookPasta() {
println("Chef is cooking pasta 🍝")
}
func (c *Chef) UndoPasta() {
println("Undo: Cancel pasta order ❌🍝")
}
func (c *Chef) CookPizza() {
println("Chef is cooking pizza 🍕")
}
func (c *Chef) UndoPizza() {
println("Undo: Cancel pizza order ❌🍕")
}
Step 3: Update Concrete Commands
Each command now implements both Execute and Undo.
type PastaOrder struct {
chef *Chef
}
func (p *PastaOrder) Execute() {
p.chef.CookPasta()
}
func (p *PastaOrder) Undo() {
p.chef.UndoPasta()
}
type PizzaOrder struct {
chef *Chef
}
func (p *PizzaOrder) Execute() {
p.chef.CookPizza()
}
func (p *PizzaOrder) Undo() {
p.chef.UndoPizza()
}
Step 4: Enhance the Invoker (Waiter)
We now track history to support undo (LIFO order).
type Waiter struct {
history []Command
}
func (w *Waiter) TakeOrder(cmd Command) {
cmd.Execute()
w.history = append(w.history, cmd)
}
func (w *Waiter) UndoLast() {
if len(w.history) == 0 {
println("No orders to undo")
return
}
lastIndex := len(w.history) - 1
lastCommand := w.history[lastIndex]
// there is no extra code to undo the command; hence decoupled from the Waiter
lastCommand.Undo()
w.history = w.history[:lastIndex]
}
Step 5: Client Usage
func main() {
chef := &Chef{}
pasta := &PastaOrder{chef: chef}
pizza := &PizzaOrder{chef: chef}
waiter := &Waiter{}
waiter.TakeOrder(pasta)
waiter.TakeOrder(pizza)
// Undo last order (pizza)
waiter.UndoLast()
// Undo another (pasta)
waiter.UndoLast()
}
Conclusion
The Command Pattern is a clean and practical way to decouple the “what” from the “how” in your code. By turning requests into objects, you gain flexibility in how actions are triggered, stored, and managed—whether that’s queuing jobs, adding undo/redo support, or extending functionality without modifying existing logic. In Go, this pattern feels especially natural due to its simple interface system and composition-first design. While it may introduce a bit of extra structure, the payoff becomes clear as your system grows in complexity, making your codebase more modular, maintainable, and easier to evolve.
References
- Head First Design Patterns



