Implementing a Command Handler Pipeline: A Guide for Clean Software Architecture

In software development, adhering to the Single Responsibility Principle (SRP) is crucial for creating maintainable and scalable code. One effective design pattern that helps achieve SRP is the chain of command pattern. I find this pattern straightforward, intuitive, and incredibly useful when paired with the concept of a "pipeline" and the traditional factory pattern.

Building the Pipeline Structure

The pipeline acts as a container for a set of commands, which allows for the grouping of commands for different purposes, such as Order Processing and Email Notification.

Example Use Case

Consider a domain where scenarios like Order Processing, Payment Capture, or User Registration are common:

var context = new Context();
context.Request.Type = "NewOrder";
        

Command Handlers

Each command in our architecture is tasked with a specific part of an operation, encapsulating the business logic necessary for each step:

var orderProcessingHandler = new OrderProcessingHandler();
var paymentCaptureHandler = new PaymentCaptureHandler();
var emailNotificationHandler = new EmailNotificationHandler();
var errorHandler = new ErrorHandler();
        

Pipelines

Pipelines enable a flexible combination of various commands. Command handlers can be reused across different pipelines, enhancing modularity and reusability:

paymentCaptureHandler.Next(ctx => ctx.Response.Success ? emailNotificationHandler : errorHandler);

var orderProcessingPipeline = new CommandHandlerPipeline(() =>
{
    orderProcessingHandler.Next(ctx => paymentCaptureHandler);
    return orderProcessingHandler;
});
        

The Factory Pattern

A factory is then used to instantiate specific command handler pipelines based on the request type, ensuring that the correct pipeline is used for each operation:

var factory = new HandlerPipelineFactory(new Dictionary<PipelineCommandHandlerType, ICommandHandlerPipeline>()
{
    [PipelineCommandHandlerType.NewOrder] = orderProcessingPipeline,
});

// Execute the pipeline for a new order.
factory.Get(PipelineCommandHandlerType.NewOrder).RunAsync(context);
        

Core Components

To implement this architecture, here are the essential classes and interfaces you will need:

public class HandlerPipelineFactory
{
    private Dictionary<PipelineCommandHandlerType, ICommandHandlerPipeline> _pipelines;

    public HandlerPipelineFactory(Dictionary<PipelineCommandHandlerType, ICommandHandlerPipeline> pipelines)
    {
        _pipelines = pipelines;
    }

    public ICommandHandlerPipeline Get(PipelineCommandHandlerType name)
    {
        return _pipelines.GetValueOrDefault(name);
    }
}

public enum PipelineCommandHandlerType
{
    NewOrder
}

public interface ICommandHandlerPipeline
{
    Task RunAsync(Context context);
}

public class CommandHandlerPipeline : ICommandHandlerPipeline
{
    private Func<CommandHandler> _rootHandler;

    public CommandHandlerPipeline(Func<CommandHandler> rootHandler)
    {
        _rootHandler = rootHandler;
    }

    public async Task RunAsync(Context context)
    {
        await _rootHandler().HandleRequestAsync(context);
    }
}

public abstract class CommandHandler
{
    private Func<Context, CommandHandler> _nextHandler;

    public void Next(Func<Context, CommandHandler> nextHandler)
    {
        _nextHandler = nextHandler;
    }

    public abstract Task HandleRequestAsync(Context context);

    protected async Task PassToNextHandlerAsync(Context context)
    {
        var nextHandler = _nextHandler?.Invoke(context);
        if (nextHandler != null)
        {
            await nextHandler.HandleRequestAsync(context);
        }
    }
}

public class Request
{
    public string Type { get; set; }
}

public class Response
{
    public string Type { get; set; }
    public bool Success { get; set; }
}

public class Context
{
    public Request Request { get; } = new Request();
    public Response Response { get; } = new Response();
}
        

This structured approach not only enhances code maintainability but also ensures that each component of our system is responsible for a single functionality, aligning perfectly with the Single Responsibility Principle.

Good reading. Well documented. Thanks for publishing Arif Y. .

To view or add a comment, sign in

More articles by Arif Y.

  • Forma, React Form/Validation Framework.

    Forma is a small framework that makes form development easier if you use React as your JavaScript library. As you may…

    3 Comments
  • Domain Events with DotNetCommander

    In my first article for DotnetCommander, I briefly described the basic usage. In this article, I will show you how to…

  • DotNetCommander - Introduction

    DotNetCommander is a small library combines command pattern, with DDD practices. The goal of commander is to help you…

  • Testing with DotNetRuleEngine

    One of the goals of DotNetRuleEngine is to encourage writing testable code. DI support allows you to easily replace…

  • ASP.NET Core Custom Policy Based Authorization

    In the last article, I've demonstrated the cookie authentication and policy based authorization. We're going to build…

  • ASP.NET Core Cookie Middleware & Policy Based Authorization

    This is the first part of the series of articles I'll be covering about ASP.NET Core Security.

    4 Comments
  • Managing Software Complexity

    Managing Software Complexity 20 OCTOBER 2016 As a developer you deal with a lot of complexities. Every organization has…

Explore content categories