The command pattern is also one of the design patterns from the Gang Of Four design patterns. The command pattern is a behavioral design pattern.
And the primary intent of this pattern is to encapsulate a request as an object, thereby letting us parameterize clients with different requests queue or log requests and support undoable operations.
From the above definition, it is not very clear what the purpose of the command pattern is. In very simple language, this pattern mainly helps decouple the invoker of a request from the receiver of the request.
The use case for Command Pattern
Let us understand this pattern through an example. In this example, we will have a distributed system. And in this distributed system, we will have two main services.
- The first one is order service
- And the second one is inventory service.
Proxy Classes
Firstly, I will create proxy classes, which will be the receiver of a request in the pattern.
And we will have a couple of proxy classes for the order service in the inventory service, and they are InventoryProxy
and OrderProxy
.
Both the InventoryProxy
class and OrderProxy
class will implement the IProxy
interface.
The code
namespace CommandPattern.Demo;
public interface IProxy<T>
{
bool Create(T item);
bool Delete(T item);
}
public class InventoryProxy : IProxy<Inventory>
{
public bool Create(Inventory inventory)
{
Console.WriteLine($"Created inventory for product: {inventory.ProductName}");
return true;
}
public bool Delete(Inventory inventory)
{
Console.WriteLine($"Deleted inventory for product: {inventory.ProductName}");
return true;
}
}
public record Inventory(string ProductName, int Quentity);
public class OrderProxy : IProxy<Order>
{
public bool Create(Order order)
{
Console.WriteLine($"Created order for product: {order.ProductName}");
return true;
}
public bool Delete(Order order)
{
Console.WriteLine($"Deleted order for product: {order.ProductName}");
return true;
}
}
public record Order(string ProductName, int Quantity);
Explanation of proxy classes
The IProxy
interface has two methods, Create
and Delete
. And for both methods, the parameter is a generic type. For the InventoryProxy
class, this parameter will be of record type Inventory
. And for the OrderProxy
class, this parameter will be of record type Order
.
For the simplicity of the example, in these methods, I am just doing Console.WriteLine
. But as you can understand in a real-life scenario these methods will be calling out to another couple of HTTP services to execute some functionality.
The Orchestrator
Now, we want to make an orchestrator, which will be responsible for creating an order and then creating an inventory. But in case of any error with creating inventory, it is going to roll back the order.
One way to do this is for the orchestrator to directly call inventory and order proxy classes. Or the better approach will be to decouple the orchestrator from the actual implementation. And this is where we can implement the command pattern.
For the command design pattern, there are four main components.
- The first component is a command which decouples the receiver from the invoker
- And then the second is the invoker which will be invoking the command
- Thirdly, the receiver is the one who will be finally executing the command
- Fourthly, there is a concept of a client who will create the command
And in this example, the OrderProxy and the InventoryProxy are nothing but the receivers.
The command classes
namespace CommandPattern.Demo;
public interface IMessage { }
public interface ICommand
{
bool Execute(IMessage message);
bool Rollback(IMessage message);
}
The interface ICommand
has two methods Execute
and Rollback
. And both of these methods will return a boolean and has a single parameter of type IMessage
. From an orchestrator’s point of view, it is just going to either execute or roll back if the execution is unsuccessful.
And IMessage
is just an empty type because what we are going to do is in the proxy classes the records Inventory
and Order
are going to implement the IMessage
interface.
public record Inventory(string ProductName, int Quentity) : IMessage;
public record Order(string ProductName, int Quantity) : IMessage;
Next, I am going to create concrete command classes for inventory and order. For the command pattern, the command class is the one that decouples the invoker from the receiver. And in this case, the receiver is nothing but the proxy classes.
OrderCommand class
public class OrderCommand : ICommand
{
private readonly IProxy<Order> orderProxy;
public OrderCommand(IProxy<Order> orderProxy)
{
this.orderProxy = orderProxy;
}
public bool Execute(IMessage message)
{
return orderProxy.Create((Order)message);
}
public bool Rollback(IMessage message)
{
return orderProxy.Delete((Order)message);
}
}
The constructor of the OrderCommand
class will take IProxy<Order> as an input parameter. Next, inside the Execute
method, we will call the Create
method of the OrderProxy
. And inside the Rollback
method, we will call the Delete
method of the OrderProxy
. In both cases, we will typecast the IMessage
to the record type Order
.
During Rollback we call Delete because it’s a compensating transaction. And that’s what happens when you are dealing with a distributed transaction, we don’t roll back, we just perform a compensating transaction.
InventoryCommand class
We are going to have a similar class for the inventory command.
public class InventoryCommand : ICommand
{
private readonly IProxy<Inventory> inventoryProxy;
public InventoryCommand(IProxy<Inventory> inventoryProxy)
{
this.inventoryProxy = inventoryProxy;
}
public bool Execute(IMessage message)
{
return inventoryProxy.Create((Inventory)message);
}
public bool Rollback(IMessage message)
{
return inventoryProxy.Delete((Inventory)message);
}
}
The code here will be identical to the OrderProxy
. The only difference is that the type of proxy is Inventory
and we will cast the IMessage
to Inventory
.
So now we have the command, that takes the receiver to execute the function. And then we have to have the invoker, and for the invoker, we are going to create an orchestrator.
Orchestrator class
namespace CommandPattern.Demo;
public interface IOrchestrator
{
bool CreateOrder(Order order);
}
public class Orchestrator : IOrchestrator
{
private readonly ICommand orderCommand;
private readonly ICommand inventoryCommand;
public Orchestrator(ICommand orderCommand, ICommand inventoryCommand)
{
this.orderCommand = orderCommand;
this.inventoryCommand = inventoryCommand;
}
public bool CreateOrder(Order order)
{
if (orderCommand.Execute(order))
{
if (inventoryCommand.Execute(new Inventory(order.ProductName, order.Quantity)))
{
return true;
}
else
{
return orderCommand.Rollback(order);
}
}
return false;
}
}
The orchestrator class has a single method CreateOrder
, which will take an order object as a parameter. And this is where we are going to use command instead of directly calling the proxies. That is how we will achieve the decoupling.
In the constructor of this class, we are going to take the ICommand
of type Order
and the ICommand
of type Inventory
.
And inside the CreateCommand
method, we will first call Execute
on the OrderCommand
. And only if that is successful, we will call the Execute
on the InventoryCommand
.
But if the Execute
call of InventoryCommand
fails, we will call the Rollback
on the OrderCommand
.
So this is a simple example of a distributed transaction using a command pattern.
The client of the command pattern
Now finally we are going to create the client of the command pattern. And the client here is nothing but the API for executing the order.
And for the API we will use the minimal API construct in the program class.
app.MapPost("/api/order", ([FromBody] Order order, [FromServices] IOrchestrator orchestrator) =>
{
return orchestrator.CreateOrder(order);
});
The API “api/order” will call the CreateOrder
of the Orchestrator
class. And the API will pass the Order
object from the request body.
Next, I will add all the dependencies needed to the dependency injection container. With all the updates the Program
class will look like below:
using CommandPattern.Demo;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IProxy<Order>, OrderProxy>();
builder.Services.AddSingleton<IProxy<Inventory>, InventoryProxy>();
builder.Services.AddSingleton<IOrchestrator>(x => new Orchestrator(
new OrderCommand(x.GetService<IProxy<Order>>()),
new InventoryCommand(x.GetService<IProxy<Inventory>>())
));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapPost("/api/order", ([FromBody] Order order, [FromServices] IOrchestrator orchestrator) =>
{
return orchestrator.CreateOrder(order);
});
app.Run();
Running the command pattern implementation
After running the program, we can use the Swagger page to execute the API. And for the Order object, we will use the product name as Milk and quantity as 2 gallons.
When we execute, we can see the response body as true. And if we look into the console, we can see both print statements as expected in the output.
Next, I will change the code in such a way as to mimic a failure during inventory creation. And to do that I will return false instead of true as a return for the Create method of the InventoryProxy.
And now if we run the application and execute the API, we will see it will roll back by deleting the transactions.
Conclusion
This is how we can use the command pattern to execute a distributed transaction and decouple the actual proxy from the orchestrator through the individual command classes. I think if we use the command pattern it just encapsulates and makes the code more readable in terms of execution and rollback. And it also gives one more layer of abstraction between the actual receiver, which is the proxy, and the invoker which is the orchestrator.
A Youtube video for this implementation is available here.