The Chain Of Responsibility Pattern: How to Use, The Pros and Cons

Cagri Tezel
inventiv
Published in
4 min readSep 29, 2023

Design Patterns have been extensively used in software development since their introduction in 1995 by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (GoF) in their renowned book “Design Patterns — Elements of Reusable Object-Oriented Software.”

But first, what is a Design Pattern? A Design Pattern is a reusable solution that combines elements of code, structure, and organization to address recurring problems in software design. A well-designed software has tons of patterns in its architecture. In this article, I will talk about The Chain Of Responsibility Pattern which I found fascinating, like a puzzle.

The Chain of Responsibility pattern is a behavioral design pattern that sends requests through a chain of handlers. Handlers in the chain either process business logic or pass the request to the next handler.

There are three key components: Client, Handler and Concrete Handlers.

Client: Initiates the request and forwards it to the initial handler.

Handler: Defines a common interface. An abstract class can be implemented for the base Handle method.

Concrete Handlers: These actual handlers implement the interface/abstract class whether to process the request or forward it to the next handler in the chain.

Let’s examine an example of The Chain of Responsibility Pattern, where a user is created.

First, let me introduce our interface, IHandler<T>. I implemented it in an abstract class to handle unintended neglect situations.

public interface IHandler<T> where T : class
{
IHandler<T> SetNext(IHandler<T> next);
void Handle(T request);
}

public abstract class Handler<T> : IHandler<T> where T : class
{
protected IHandler<T> Next { get; set; }

public virtual void Handle(T request)
{
Next?.Handle(request);
}

public IHandler<T> SetNext(IHandler<T> next)
{
Next = next;

return Next;
}
}

Then, I created four handlers.

The ValidationHandler class will validate the given command attributes.

internal class ValidationHandler: Handler<CreateAccountContext>
{
public override void Handle(CreateAccountContext request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}

var command = request.Command;
if (string.IsNullOrWhiteSpace(command.FirstName))
{
throw new DomainException(DomainExceptionContent.FirstNameCannotBeEmpty);
}

if (string.IsNullOrWhiteSpace(command.LastName))
{
throw new DomainException(DomainExceptionContent.LastNameCannotBeEmpty);
}

if (string.IsNullOrWhiteSpace(command.SocialSecurityNumber))
{
throw new DomainException(DomainExceptionContent.SocialSecurityNumberCannotBeEmpty);
}

if (command.SocialSecurityNumber.Length != 11)
{
throw new DomainException(DomainExceptionContent.SocialSecurityNumberMustBeElevenChars);
}

if (string.IsNullOrWhiteSpace(command.Email))
{
throw new DomainException(DomainExceptionContent.EmailCannotBeEmpty);
}

if (command.BirthDate.Equals(default))
{
throw new DomainException(DomainExceptionContent.BirthDateCannotBeEmpty);
}

if (command.BirthDate.Year.ToString().Length != 4)
{
throw new DomainException(DomainExceptionContent.BirthYearMustBeFourChars);
}

if (string.IsNullOrWhiteSpace(command.Password.PasswordToCompare))
{
throw new DomainException(DomainExceptionContent.PasswordCannotBeEmpty);
}

if (string.IsNullOrWhiteSpace(command.Password.ComparisonEntry))
{
throw new DomainException(DomainExceptionContent.ReEnteredPasswordCannotBeEmpty);
}

if (!command.Password.PasswordToCompare.Equals(command.Password.ComparisonEntry))
{
throw new DomainException(DomainExceptionContent.PasswordMismatch);
}

base.Handle(request);
}
}

The TCKNValidationHandler class will validate the social security number.

internal class TCKNValidationHandler: Handler<CreateAccountContext>
{
private readonly ITCKNValidateService ValidationService;

public TCKNValidationHandler(ITCKNValidateService validateService)
{
ValidationService = validateService;
}

public override void Handle(CreateAccountContext request)
{
if(!ValidationService.Validate(request.Command.ToTCKNValidationRequest()))
{
throw new DomainException(DomainExceptionContent.SocialSecurityNotValidated, request);
}

base.Handle(request);
}
}

AccountHandler and PasswordHandler classes are for creating and storing Account and Password entities.

internal class AccountHandler: Handler<CreateAccountContext>
{
private readonly IAccountRepository AccountRepository;

public AccountHandler(IAccountRepository accountRepository)
{
AccountRepository = accountRepository;
}

public async override void Handle(CreateAccountContext request)
{
var account = await AccountRepository.GetOneAsync(x => x.SocialSecurityNumber == request.Command.SocialSecurityNumber);
if (account != default)
{
throw new DomainException(DomainExceptionContent.SocialSecurityAlreadyUsed);
}

request.SetAccount(await AccountRepository.AddAsync(request.Command.ToAccount()));
base.Handle(request);
}
}
internal class PasswordHandler: Handler<CreateAccountContext>
{
private readonly IPasswordRepository PasswordRepository;
public PasswordHandler(IPasswordRepository passwordRepository)
{
PasswordRepository = passwordRepository;
}

public override void Handle(CreateAccountContext request)
{
if(request.Account == default)
{
throw new DomainException(DomainExceptionContent.AccountCannotCreated);
}

var passwordHash = CryptoHelper.PasswordHash(request.Command.Password.PasswordToCompare);
PasswordRepository.AddAsync(new Entity.Password(request.Account.Id, passwordHash));

base.Handle(request);
}
}

I created a context for CreateAccountCommand to pass the Account object to PasswordHandler.

internal class CreateAccountContext
{
public CreateAccountCommand Command { get; }
public Account? Account { get; private set; }

public CreateAccountContext(CreateAccountCommand command)
{
Command = command;
}

internal void SetAccount(Account account)
{
Account = account;
}
}

At last, I created a request handler to define the chain of handlers and process the context.

public sealed class CreateAccountHandler: IRequestHandler<CreateAccountCommand, Result>
{
private readonly IAccountRepository AccountRepository;
private readonly IPasswordRepository PasswordRepository;
private readonly ITCKNValidateService TCKNValidateService;

public CreateAccountHandler(IAccountRepository accountRepository, IPasswordRepository passwordRepository, ITCKNValidateService tCKNValidateService)
{
AccountRepository = accountRepository;
PasswordRepository = passwordRepository;
TCKNValidateService = tCKNValidateService;
}

public async Task<Result> Handle(CreateAccountCommand request, CancellationToken cancellationToken)
{
var context = new CreateAccountContext(request);
var handler = new ValidationHandler();
handler.SetNext(new TCKNValidationHandler(TCKNValidateService))
.SetNext(new AccountHandler(AccountRepository))
.SetNext(new PasswordHandler(PasswordRepository));

handler.Handle(context);

return new Result(true, context.Account, default);
}
}

What have we gained here?

  • Decoupled the code and created a clean and extensible code base.
  • Every concrete handler has one single responsibility, so the Single Responsibility Principle is achieved.
  • We can extend the chain dynamically to add new handlers, and also able to remove them. This adds flexibility and scalability to our code base.
  • We can easily write unit tests for every handler separately.

Be careful when using this pattern,

  • There is always a risk of missing a chain and leaving the process unfinished. To avoid that, I used an abstract implementation of interface IHandler<T> and called the base.Handle method in each concrete handler override.
  • Keep the chain short and simple. Long and complex handler chains can slow down requests. In the ValidationHandler class, using handlers for each validation was a bad idea. (Using the FluentValidation package can be an option.)

When to Use this pattern?

  • When you need to separate the sender and receiver of a request
  • When you have multiple objects capable of handling a request without specifying the handler explicitly.
  • If you need to add or remove handlers dynamically.

The example code is here: https://github.com/doctoreee/AccountServiceVSA

(I implemented a StubTCKNValidateService for readers who are not Turkish citizens. You can inject this class in the ExternalServicesExtensions class instead of TCKNValidateService.)

--

--