Full Stack • Java • System Design • Cloud • AI Engineering

Chain of Responsibility Design Pattern in Java

Learn Chain of Responsibility Design Pattern in Java with request processing pipelines, validation chains, approval workflows, Spring Security filter chain, diagrams, code examples, benefits, limitations, and interview questions.

What You Will Learn

In this article, you'll learn:

  • What is Chain of Responsibility Pattern?
  • Why Chain of Responsibility is needed
  • Real-world examples
  • Request processing pipelines
  • Java implementation
  • Approval workflow example
  • Spring Security filter chain
  • Servlet filter chain
  • Benefits and limitations
  • Interview questions

Introduction

In enterprise applications, a request often goes through multiple steps before completion.

Example:

HTTP Request
→ Authentication
→ Authorization
→ Validation
→ Logging
→ Business Logic

Another example:

Loan Application
→ Basic Validation
→ Credit Score Check
→ Income Verification
→ Manager Approval
→ Final Approval

If all logic is written in one class, the code becomes complex and hard to maintain.

Chain of Responsibility solves this problem by passing a request through a chain of handlers.


What is Chain of Responsibility Pattern?

Chain of Responsibility is a Behavioral Design Pattern that allows a request to pass through a chain of handlers.

Each handler decides:

Can I handle this request?

Yes → Handle it

No → Pass to next handler

Purpose of Chain of Responsibility

The main purpose is:

Avoid tightly coupling sender and receiver.

Instead of one object handling everything, multiple handlers are connected in a chain.


Real World Analogy

Think about customer support.

Customer Issue
→ Level 1 Support
→ Level 2 Support
→ Manager
→ Engineering Team

Each level checks if it can resolve the issue.

If not, it forwards the issue to the next level.

This is Chain of Responsibility.


Problem Without Chain of Responsibility

public class LoanApprovalService {

    public void approveLoan(LoanApplication application) {

        if (application.getAmount() <= 0) {
            throw new RuntimeException("Invalid loan amount");
        }

        if (application.getCreditScore() < 700) {
            throw new RuntimeException("Low credit score");
        }

        if (application.getIncome() < 50000) {
            throw new RuntimeException("Low income");
        }

        System.out.println("Loan Approved");
    }
}

Problems in This Code

  • Too many conditions in one class
  • Hard to add new validation
  • Violates Single Responsibility Principle
  • Difficult to test each step independently
  • Business logic and validation logic are mixed

Solution With Chain of Responsibility

flowchart LR
    A[Loan Application] --> B[Amount Validator]
    B --> C[Credit Score Validator]
    C --> D[Income Validator]
    D --> E[Approval Handler]

Each handler has one responsibility.


Chain of Responsibility Structure

classDiagram
    class Handler {
        <<abstract>>
        - nextHandler
        + setNext(handler)
        + handle(request)
    }

    class AmountValidator
    class CreditScoreValidator
    class IncomeValidator
    class ApprovalHandler

    Handler <|-- AmountValidator
    Handler <|-- CreditScoreValidator
    Handler <|-- IncomeValidator
    Handler <|-- ApprovalHandler

    Handler --> Handler

Key Components

Handler

Common abstract class or interface.

It defines:

handle()
setNext()

Concrete Handler

Actual processing step.

Examples:

Amount Validator
Credit Score Validator
Income Validator
Approval Handler

Client

Builds the chain and sends the request.


Loan Application Example

Step 1: Create Request Object

public class LoanApplication {

    private double amount;
    private int creditScore;
    private double income;

    public LoanApplication(
            double amount,
            int creditScore,
            double income) {

        this.amount = amount;
        this.creditScore = creditScore;
        this.income = income;
    }

    public double getAmount() {
        return amount;
    }

    public int getCreditScore() {
        return creditScore;
    }

    public double getIncome() {
        return income;
    }
}

Step 2: Create Abstract Handler

public abstract class LoanHandler {

    private LoanHandler nextHandler;

    public LoanHandler setNext(
            LoanHandler nextHandler) {

        this.nextHandler = nextHandler;
        return nextHandler;
    }

    public void handle(
            LoanApplication application) {

        process(application);

        if (nextHandler != null) {
            nextHandler.handle(application);
        }
    }

    protected abstract void process(
            LoanApplication application);
}

Step 3: Amount Validator

public class AmountValidator
        extends LoanHandler {

    @Override
    protected void process(
            LoanApplication application) {

        if (application.getAmount() <= 0) {
            throw new RuntimeException(
                    "Loan amount must be greater than zero");
        }

        System.out.println("Amount validation passed");
    }
}

Step 4: Credit Score Validator

public class CreditScoreValidator
        extends LoanHandler {

    @Override
    protected void process(
            LoanApplication application) {

        if (application.getCreditScore() < 700) {
            throw new RuntimeException(
                    "Credit score is too low");
        }

        System.out.println("Credit score validation passed");
    }
}

Step 5: Income Validator

public class IncomeValidator
        extends LoanHandler {

    @Override
    protected void process(
            LoanApplication application) {

        if (application.getIncome() < 50000) {
            throw new RuntimeException(
                    "Income is too low");
        }

        System.out.println("Income validation passed");
    }
}

Step 6: Approval Handler

public class ApprovalHandler
        extends LoanHandler {

    @Override
    protected void process(
            LoanApplication application) {

        System.out.println("Loan Approved Successfully");
    }
}

Step 7: Client Code

public class ChainOfResponsibilityDemo {

    public static void main(String[] args) {

        LoanApplication application =
                new LoanApplication(
                        250000,
                        760,
                        90000);

        LoanHandler amountValidator =
                new AmountValidator();

        LoanHandler creditScoreValidator =
                new CreditScoreValidator();

        LoanHandler incomeValidator =
                new IncomeValidator();

        LoanHandler approvalHandler =
                new ApprovalHandler();

        amountValidator
                .setNext(creditScoreValidator)
                .setNext(incomeValidator)
                .setNext(approvalHandler);

        amountValidator.handle(application);
    }
}

Output

Amount validation passed
Credit score validation passed
Income validation passed
Loan Approved Successfully

Execution Flow

sequenceDiagram
    participant Client
    participant Amount
    participant Credit
    participant Income
    participant Approval

    Client->>Amount: handle(application)
    Amount->>Credit: handle(application)
    Credit->>Income: handle(application)
    Income->>Approval: handle(application)
    Approval-->>Client: Approved

Failed Request Example

Input:

LoanApplication application =
        new LoanApplication(
                250000,
                620,
                90000);

Output:

Amount validation passed
Exception: Credit score is too low

The request stops when one handler fails.


Real Enterprise Example: Approval Workflow

Expense Approval:

Expense Request
→ Team Lead
→ Manager
→ Director
→ Finance

Each approver handles based on amount.


Expense Approval Flow

flowchart LR
    A[Expense Request] --> B[Team Lead]
    B --> C[Manager]
    C --> D[Director]
    D --> E[Finance]

Banking Use Case

Transaction Processing:

Transaction Request
→ Authentication Check
→ Balance Check
→ Fraud Check
→ Limit Check
→ Transfer Execution

Banking Flow

flowchart LR
    A[Transaction] --> B[Authentication]
    B --> C[Balance Check]
    C --> D[Fraud Check]
    D --> E[Limit Check]
    E --> F[Transfer]

Insurance Use Case

Claim Processing:

Claim Request
→ Policy Validation
→ Document Verification
→ Fraud Check
→ Medical Review
→ Claim Approval

Insurance Flow

flowchart LR
    A[Claim] --> B[Policy Validation]
    B --> C[Document Check]
    C --> D[Fraud Check]
    D --> E[Medical Review]
    E --> F[Approval]

Spring Security Example

Spring Security uses filter chains.

Request flow:

HTTP Request
→ Security Filters
→ Controller

Spring Security Filter Chain

flowchart LR
    A[HTTP Request] --> B[Authentication Filter]
    B --> C[Authorization Filter]
    C --> D[CSRF Filter]
    D --> E[Controller]

Each filter processes the request and forwards it to the next filter.


Servlet Filter Chain Example

public class LoggingFilter implements Filter {

    @Override
    public void doFilter(
            ServletRequest request,
            ServletResponse response,
            FilterChain chain)
            throws IOException, ServletException {

        System.out.println("Request received");

        chain.doFilter(request, response);

        System.out.println("Response sent");
    }
}

Chain Flow in Servlet

Filter 1
→ Filter 2
→ Filter 3
→ Servlet

This is a classic Chain of Responsibility example.


API Gateway Example

API Gateway request pipeline:

Request
→ Authentication
→ Authorization
→ Rate Limiting
→ Logging
→ Routing
→ Microservice

API Gateway Flow

flowchart LR
    A[Request] --> B[Authentication]
    B --> C[Authorization]
    C --> D[Rate Limiting]
    D --> E[Logging]
    E --> F[Routing]
    F --> G[Microservice]

Framework Examples

Framework Chain Example
Spring Security Security Filter Chain
Servlet API FilterChain
Spring MVC Handler Interceptors
Apache Camel Route Processing
Netty Channel Pipeline
API Gateway Middleware Chain

Benefits

✅ Reduces coupling between sender and receiver

✅ Improves code maintainability

✅ Each handler has one responsibility

✅ Easy to add or remove steps

✅ Good for validation pipelines

✅ Good for workflow processing

✅ Supports Open Closed Principle


Limitations

❌ Request may go unhandled

❌ Debugging can be harder

❌ Chain order matters

❌ Too many handlers can increase complexity


When To Use

Use Chain of Responsibility when:

  • Multiple handlers may process a request
  • Request processing has ordered steps
  • You need validation pipelines
  • You want loosely coupled handlers
  • You need flexible workflow processing

When Not To Use

Avoid this pattern when:

  • Only one object handles the request
  • Processing order is fixed and simple
  • Chain adds unnecessary complexity

Chain of Responsibility vs Decorator

Feature Chain of Responsibility Decorator
Goal Pass request through handlers Add behavior to object
Flow Handler to handler Wrapper to wrapped object
Request Handling May stop anytime Usually all decorators execute
Example Security Filters Java IO Streams

Chain of Responsibility vs Command

Feature Chain of Responsibility Command
Purpose Request processing pipeline Encapsulate request as object
Focus Handler chain Action object
Example Approval workflow Undo/Redo operations

Interview Questions

What is Chain of Responsibility Pattern?

A behavioral pattern where a request passes through a chain of handlers until it is handled.


Real World Example?

Customer support escalation or loan approval workflow.


Where is it used in Java?

Servlet FilterChain and Spring Security Filter Chain.


What problem does it solve?

It avoids coupling sender and receiver and separates request processing steps.


Can a request stop in the middle?

Yes. A handler can stop processing if validation fails or if it fully handles the request.


Why is order important?

Because each handler may depend on the result of previous handlers.


Key Takeaways

  • Chain of Responsibility is a Behavioral Design Pattern.
  • It passes requests through a chain of handlers.
  • Each handler has one responsibility.
  • Handlers can process, forward, or stop the request.
  • Commonly used in validation, security, approval workflows, and middleware pipelines.
  • Spring Security and Servlet FilterChain are classic real-world examples.
  • It improves flexibility and reduces tight coupling.