In our previous post, we explored the Pipeline pattern for data processing. But what if you need to treat an entire business action—like "Cancel Subscription" or "Apply Discount"—as a first-class object? The Command Pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue actions, and even support undoable operations.
The Business Use Case: Multi-Step Order Cancellation
Canceling an order in a modern e-commerce system isn't just a database update. You have to:
- Check if the order is already shipped.
- Refund the payment via a gateway.
- Re-stock the inventory.
- Send a confirmation email.
By turning this into a Command, you can easily log exactly who triggered the cancellation, when it happened, and even "undo" it if the customer changes their mind.
The Problem: The Bloated Service Function
# The "Bad" Way: Hard-coded logic that is difficult to log or undo
def cancel_order_view(request, order_id):
order = Order.objects.get(id=order_id)
# Logic is trapped in the view or a rigid service
order.status = 'cancelled'
gateway.refund(order.payment_id)
inventory.restock(order.items)
order.save()
# How do we easily track this action or revert it?
If you need to add "Audit Logging" to every major action in your app, you’ll end up copy-pasting logging code into every single service function.
The Solution: The Command Pattern
We wrap the action into a Command object. This object contains all the information needed to execute the task later or track its history.
Step 1: Define the Command Interface
Every command must have an execute method. Optionally, we add undo.
# commands/base.py
class Command:
def execute(self):
raise NotImplementedError()
def undo(self):
raise NotImplementedError()
Step 2: Create the Concrete Command
The command carries the "Receiver" (the object it acts upon) and the data needed.
# commands/order_commands.py
class CancelOrderCommand(Command):
def __init__(self, order_id):
self.order_id = order_id
self._original_status = None
def execute(self):
order = Order.objects.get(id=self.order_id)
self._original_status = order.status
# Perform the actual business logic
order.cancel_and_refund()
return f"Order {self.order_id} cancelled."
def undo(self):
# Reverse the action
order = Order.objects.get(id=self.order_id)
order.restore(self._original_status)
Step 3: The New Workflow (Invoker)
The service layer or view now just "invokes" the command. This makes it easy to add logging or background queueing globally.
# services.py
def handle_user_request(command: Command):
# We can log the command here for an audit trail
AuditLogger.log(f"Executing: {command.__class__.__name__}")
result = command.execute()
return result
Why it's Better for Big Projects
- Audit Trails: Since every action is an object, you can save a history of every command executed in your system to a database table.
- Undo/Redo: Because the command stores the "previous state," reversing a complex action becomes trivial.
- Deferred Execution: Commands can be serialized and sent to a Celery worker to be executed later without changing the command's code.
The Command Pattern turns "doing something" into "an object that represents doing something." This level of abstraction is vital for systems that require high accountability, background processing, or complex workflows.
Architecture Note: To keep your codebase clean and avoid circular import errors, keep your commands/ directory separate from your models. This allows your Service Layer to treat actions as modular, interchangeable units, making your application significantly more robust and easier to debug.