Chain of Responsibility Design Pattern explained in 2 minutes

Practical guide to explain Chain of Responsibility Design Pattern

Play this article

Problem Statement

In real-world applications, we frequently have to run a series of validations to ensure our model class is properly created before we persist it in our database. Consider the below Employee class

public record Employee(int employeeId, String firstName, String lastName, int salary, int managerId, int age) {
}

If we have to run validations to ensure whether the names are properly set, the age is valid or a valid employee ID has been assigned before we persist our POJO into a database, what's the simplest way?

Presenting the humble If/else!

  if (validEmployee.employeeId() != 0 
        && !validEmployee.firstName().isEmpty() 
        && !validEmployee.lastName().isEmpty() 
        && validEmployee.age() >= 18) {
      return true;
  }

What red flag do you see in the above code? Although it's simpler, it violates OCP (Open/Closed Principle). The code is not extensible and will require frequent modifications.

Chain of Responsibility Design Pattern can help tidy up the code.

public abstract class Validator {
    public Validator nextValidator;

    public Validator setNextValidator(Validator next) {
        this.nextValidator = next;
        return this;
    }

    public abstract boolean isValid(Employee employee);
}

public class EmployeeIdValidator extends Validator {
    @Override
    public boolean isValid(Employee employee) {
        System.out.println("Running Employee ID Validator");
        if (nextValidator == null) {
            // if there is no next validator in the chain
            return isIdValid(employee.employeeId());
        } else if (isIdValid(employee.employeeId())) {
            // delegate to the next validator
            return nextValidator.isValid(employee);
        } else {
            System.out.println("Employee ID is invalid");
            return false;
        }
    }

    private boolean isIdValid(int id) {
        return id != 0;
    }
}

public class NameValidator extends Validator {
    @Override
    public boolean isValid(Employee employee) {
        System.out.println("Running Name Validator");
        if (nextValidator == null) {
            return isNameValid(employee);
        } else if (isNameValid(employee)) {
            return nextValidator.isValid(employee);
        } else {
            System.out.println("Employee name is invalid");
            return false;
        }
    }

    private static boolean isNameValid(Employee employee) {
        return !employee.firstName().isBlank()
                && !employee.firstName().isEmpty()
                && !employee.lastName().isEmpty()
                && !employee.lastName().isBlank();
    }
}

public class AgeValidator extends Validator {
    @Override
    public boolean isValid(Employee employee) {
        System.out.println("Running Age Validator");
        if (nextValidator == null) {
            return isAgeValid(employee.age());
        } else if (isAgeValid(employee.age())) {
            return nextValidator.isValid(employee);
        } else {
            System.out.println("Age is not valid");
            return false;
        }
    }

    private boolean isAgeValid(int age) {
        return age >= 18 && age <= 70;
    }
}

public class Main {
    public static void main(String[] args) {
        testChainOfResponsibility();
    }

    public static void testChainOfResponsibility() {
        Employee validEmployee = new Employee(1, "Snehasish", "Roy", 100, 100, 20);
        // Chain the validators
        Validator validatorChain = new EmployeeIdValidator()
                .setNextValidator(new AgeValidator()
                        .setNextValidator(new NameValidator()));
        System.out.println(validatorChain.isValid(validEmployee));

        Employee invalidEmployee = new Employee(1, "Snehasish", "Roy", 100, 100, 10);
        System.out.println(validatorChain.isValid(invalidEmployee));
    }
}

// Output
Running Employee ID Validator
Running Age Validator
Running Name Validator
true

Running Employee ID Validator
Running Age Validator
Age is not valid
false

In the code above, we created dedicated validators for handling only one validation at a time. Each validator first performs local validations and then delegates to the next validator (if any).

What's the benefit?

  • Follows OCP - Any modification/extension in the validation logic of a validator will require a change in only one class.

  • Follows SRP (Single Responsibility Principle) - Each validator performs only one task.

What's the drawback?

  • Validators must be chained correctly - if there are any cycles in the chain, then it can cause issues at runtime.

  • The responsibility of initializing validators lies with the client. The client can either create a chain at the compile time or dynamically update the chain as per the business logic.

Did you find this article valuable?

Support Snehasish Roy by becoming a sponsor. Any amount is appreciated!