Let’s imagine we’re building a SaaS application about document processing. Our application needs to handle various tasks such as spell checking, grammar checking, GDPR compliance checks, and saving documents. Each of these tasks can be thought of as a step in a workflow. Managing this workflow efficiently is crucial for the performance and maintainability of our application.
One effective way to handle this kind of workflow is by using the Chain of Responsibility (CoR) pattern. This pattern allows us to pass a request along a chain of handlers, each of which can process the request or pass it to the next handler in the chain. This decouples the sender of the request from its receivers, promoting a clean and modular design.
What is the Chain of Responsibility Pattern?
The Chain of Responsibility pattern is a behavioral design pattern that lets you pass requests along a chain of handlers. Each handler in the chain has the opportunity to process the request or pass it to the next handler. This pattern is useful when you want to decouple the sender of a request from its handlers, allowing multiple handlers to process the request in a flexible and reusable way.
Key Benefits
Decoupling: The sender of a request is decoupled from the handlers, which can be modified or extended independently.
Flexibility: Handlers can be added or removed dynamically without changing the client code.
Single Responsibility Principle: Each handler only focuses on one specific aspect of the request processing.
Example
Let’s imagine that Documentoo, our fictional SaaS company, has gathered the requirements and decided to design the system using the Chain of Responsibility pattern. This approach allows us to add additional layers, or steps, to our document processing pipeline seamlessly.
Here’s how it works: if a user wants to add a GDPR compliance check, they simply click a button, and it's added to their workflow. If another user wants both GDPR compliance and grammar checking, they can set it up just as easily.
I love this pattern because it aligns perfectly with the Open-Closed Principle. This principle states that a class should be open for extension but closed for modification.
The Open-Closed Principle states that a class should be open for extension but closed for modification.
What does this mean for us? At any point, we can add new functionality without altering any existing classes. We just create a new class and integrate it into our pipeline.
This pattern also adheres to the Single Responsibility Principle, which states that a class should have only one reason to change. By using the Chain of Responsibility pattern, each handler class is responsible for a single specific task, making the code more modular and easier to maintain.
The Single Responsibility Principle states that a class should have only one reason to change.
Another great feature of this pattern is its flexibility in handling issues. If there’s a problem with GDPR compliance, for example, we can short-circuit the pipeline. This means that if any step fails, the process stops immediately, preventing further steps from executing. It ensures that we don’t waste resources continuing a process that’s already destined to fail.
Now, let’s take a look at a diagram representing the pattern applied for our use-case.
As you can see, we have the input, which is our document that we want to evaluate based on our business case and policy. We have four handlers, and each one can either pass the document to the next handler or, if there's an issue at the current step, short-circuit the process and return early.
Tomorrow, let’s say the business wants to add another step – language translation to a language specified by the user. We could easily add another handler, or step, in our pipeline that handles this new requirement.
This flexibility is one of the key benefits of using the Chain of Responsibility pattern: we can add new functionality without modifying the existing handlers. We just create a new handler for the language translation and integrate it into our pipeline.
Code example in C#
Let’s now implement this in C# and see how it works in practice. The code will be available on GitHub (see the link at the end of the post).
First, let’s take a quick look at the UML diagram.
The diagram illustrates how the Chain of Responsibility pattern works in our document processing scenario. The Client
sends a document to the first Handler
. Each Handler
(e.g., Grammar Check, GDPR Check) has two key methods: HandleRequest()
to process the document and SetNextHandler()
to link to the next handler in the chain.
This setup allows the document to be passed through each handler in sequence. If a handler encounters an issue, it can short-circuit the process; otherwise, it passes the document to the next handler.
Let’s now build this in C#.
Document
This represents our document.
public class Document { }
Base Handler
public abstract class DocumentHandler
{
public DocumentHandler _next;
protected DocumentHandler(DocumentHandler next)
{
_next = next;
}
public void SetNextHandler(DocumentHandler next)
{
_next = next;
}
public abstract void HandleRequest(Document document);
}
The code defines an abstract base class DocumentHandler
for handling document processing in a Chain of Responsibility pattern.
The class includes:
A protected field
_next
that holds a reference to the next handler in the chain.A constructor that initializes the
_next
handler.A method
SetNextHandler
to set the next handler dynamically.An abstract method
HandleRequest
, which must be implemented by concrete subclasses to process the document.
This setup allows each handler to process a document and then pass it to the next handler in the chain.
Let’s now create our concrete handlers for grammar checking, GDPR compliance, copyright infringement, and sensitive information checking.
public class GrammarCheckHandler : DocumentHandler
{
public GrammarCheckHandler(DocumentHandler next) : base(next) { }
public override void HandleRequest(Document document)
{
Console.WriteLine("Checking for grammar mistakes...");
if (_next != null)
{
_next.HandleRequest(document);
}
}
}
public class GDPRComplianceCheckHandler : DocumentHandler
{
public GDPRComplianceCheckHandler(DocumentHandler next) : base(next) { }
public override void HandleRequest(Document document)
{
Console.WriteLine("Checking for GDPR compliance...");
if (_next != null)
{
_next.HandleRequest(document);
}
}
}
public class CopyrightInfringementCheckHandler : DocumentHandler
{
public CopyrightInfringementCheckHandler(DocumentHandler next) : base(next) { }
public override void HandleRequest(Document document)
{
Console.WriteLine("Checking for copyright infringement...");
if (_next != null)
{
_next.HandleRequest(document);
}
}
}
public class SensitiveInformationCheckHandler : DocumentHandler
{
public SensitiveInformationCheckHandler(DocumentHandler next) : base(next) { }
public override void HandleRequest(Document document)
{
Console.WriteLine("Checking for sensitive information (passwords, certificates, etc.)...");
if (_next != null)
{
_next.HandleRequest(document);
}
}
}
Now what’s left is to run our pipeline.
Program.cs
class Program
{
static void Main(string[] args)
{
Document document = new Document();
// Chain the handlers in the correct order
DocumentHandler handlerChain = new GrammarCheckHandler(
new GDPRComplianceCheckHandler(
new CopyrightInfringementCheckHandler(
new SensitiveInformationCheckHandler(null))));
// Start the processing with the first handler
handlerChain.HandleRequest(document);
}
}
Output
As you can see, all of our steps (handlers) were executed on our document. You can always add logic to short-circuit the process if there are issues with any of the steps. For example, if the document is not GDPR compliant, there's no need to continue with further processing—we must fix that issue before proceeding.
Conclusion
I really like this pattern because it is versatile and can be applied in many different ways in your solutions. It is widely used in various places, from libraries like MediatR to many components within the .NET framework itself, such as the middleware pipeline in ASP.NET Core, which uses a variation of it.
Every time you have a problem that requires handling requests through a series of steps or checks, the Chain of Responsibility pattern can be a great solution.
You can find the source code on GitHub here: https://github.com/kaldren/DesignPatterns101