Fundamentals 13 min read

Understanding and Refactoring the Chain of Responsibility Design Pattern in Java

This article explains the Chain of Responsibility pattern, its typical use cases, demonstrates a problematic nested‑if implementation for a multi‑level game, and shows step‑by‑step refactoring to a clean, extensible chain using abstract handlers, concrete handlers, and a factory‑based configuration in Java.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Understanding and Refactoring the Chain of Responsibility Design Pattern in Java

Hello everyone, I'm Chen. Recently I asked a team member to implement an import feature, and they used the Chain of Responsibility pattern, resulting in overly complex code and many bugs. I realized that for this import functionality, the Template Method pattern would be more appropriate, and we conducted a collective code review.

What is the Chain of Responsibility? It is a behavioral design pattern that allows a request to be passed along a chain of handlers. Each handler can process the request or forward it to the next handler in the chain.

Typical usage scenarios:

Multi‑condition flow control, such as permission checks.

ERP system workflow approvals (e.g., general manager, HR manager, project manager).

Underlying implementation of Java filters (e.g., Servlet Filter).

If the pattern is not used, changes in requirements can quickly make the code bloated and hard to maintain. Below is an anti‑pattern example using nested if statements for a three‑level game:

// First level
public class FirstPassHandler {
    public int handler() {
        System.out.println("First level --> FirstPassHandler");
        return 80;
    }
}

// Second level
public class SecondPassHandler {
    public int handler() {
        System.out.println("Second level --> SecondPassHandler");
        return 90;
    }
}

// Third level
public class ThirdPassHandler {
    public int handler() {
        System.out.println("Third level --> ThirdPassHandler, this is the last level");
        return 95;
    }
}

// Client
public class HandlerClient {
    public static void main(String[] args) {
        FirstPassHandler firstPassHandler = new FirstPassHandler();
        SecondPassHandler secondPassHandler = new SecondPassHandler();
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();
        int firstScore = firstPassHandler.handler();
        if (firstScore >= 80) {
            int secondScore = secondPassHandler.handler();
            if (secondScore >= 90) {
                thirdPassHandler.handler();
            }
        }
    }
}

When the number of levels grows (e.g., 100 levels), the code becomes deeply nested and hard to maintain:

if (level1Passed) {
    // level 2
    if (level2Passed) {
        // level 3
        if (level3Passed) {
            // ...
        }
    }
}

This approach is redundant and risky to modify. To improve, we can connect each level via a linked list, forming a proper chain.

Initial refactor using a chain:

public class FirstPassHandler {
    /**
     * The next level is SecondPassHandler
     */
    private SecondPassHandler secondPassHandler;

    public void setSecondPassHandler(SecondPassHandler secondPassHandler) {
        this.secondPassHandler = secondPassHandler;
    }

    private int play() { return 80; }

    public int handler() {
        System.out.println("First level --> FirstPassHandler");
        if (play() >= 80) {
            if (this.secondPassHandler != null) {
                return this.secondPassHandler.handler();
            }
        }
        return 80;
    }
}

public class SecondPassHandler {
    /**
     * The next level is ThirdPassHandler
     */
    private ThirdPassHandler thirdPassHandler;

    public void setThirdPassHandler(ThirdPassHandler thirdPassHandler) {
        this.thirdPassHandler = thirdPassHandler;
    }

    private int play() { return 90; }

    public int handler() {
        System.out.println("Second level --> SecondPassHandler");
        if (play() >= 90) {
            if (this.thirdPassHandler != null) {
                return this.thirdPassHandler.handler();
            }
        }
        return 90;
    }
}

public class ThirdPassHandler {
    private int play() { return 95; }

    /**
     * This is the last level, so there is no next handler
     */
    public int handler() {
        System.out.println("Third level --> ThirdPassHandler, this is the last level");
        return play();
    }
}

public class HandlerClient {
    public static void main(String[] args) {
        FirstPassHandler firstPassHandler = new FirstPassHandler();
        SecondPassHandler secondPassHandler = new SecondPassHandler();
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();
        firstPassHandler.setSecondPassHandler(secondPassHandler);
        secondPassHandler.setThirdPassHandler(thirdPassHandler);
        firstPassHandler.handler();
    }
}

Although this reduces nested if s, each concrete handler still holds a reference to a specific next handler, making the chain hard to extend.

Full Chain of Responsibility structure:

Abstract Handler (Handler): defines the handling method and a reference to the next handler.

Concrete Handlers: implement the handling logic and decide whether to pass the request forward.

Client: builds the chain and initiates the request.

public abstract class AbstractHandler {
    /** Next handler */
    protected AbstractHandler next;

    public void setNext(AbstractHandler next) {
        this.next = next;
    }

    public abstract int handler();
}

public class FirstPassHandler extends AbstractHandler {
    private int play() { return 80; }

    @Override
    public int handler() {
        System.out.println("First level --> FirstPassHandler");
        int score = play();
        if (score >= 80 && this.next != null) {
            return this.next.handler();
        }
        return score;
    }
}

public class SecondPassHandler extends AbstractHandler {
    private int play() { return 90; }

    @Override
    public int handler() {
        System.out.println("Second level --> SecondPassHandler");
        int score = play();
        if (score >= 90 && this.next != null) {
            return this.next.handler();
        }
        return score;
    }
}

public class ThirdPassHandler extends AbstractHandler {
    private int play() { return 95; }

    @Override
    public int handler() {
        System.out.println("Third level --> ThirdPassHandler");
        int score = play();
        if (score >= 95 && this.next != null) {
            return this.next.handler();
        }
        return score;
    }
}

public class HandlerClient {
    public static void main(String[] args) {
        FirstPassHandler first = new FirstPassHandler();
        SecondPassHandler second = new SecondPassHandler();
        ThirdPassHandler third = new ThirdPassHandler();
        first.setNext(second);
        second.setNext(third);
        first.handler();
    }
}

Factory‑based chain construction (using enums and configuration):

public enum GatewayEnum {
    API_HANDLER(new GatewayEntity(1, "API rate limit", "cn.dgut.design.chain_of_responsibility.GateWay.impl.ApiLimitGatewayHandler", null, 2)),
    BLACKLIST_HANDLER(new GatewayEntity(2, "Blacklist filter", "cn.dgut.design.chain_of_responsibility.GateWay.impl.BlacklistGatewayHandler", 1, 3)),
    SESSION_HANDLER(new GatewayEntity(3, "Session filter", "cn.dgut.design.chain_of_responsibility.GateWay.impl.SessionGatewayHandler", 2, null));

    private GatewayEntity gatewayEntity;
    GatewayEnum(GatewayEntity gatewayEntity) { this.gatewayEntity = gatewayEntity; }
    public GatewayEntity getGatewayEntity() { return gatewayEntity; }
}

public class GatewayEntity {
    private String name;
    private String conference; // fully‑qualified class name
    private Integer handlerId;
    private Integer preHandlerId;
    private Integer nextHandlerId;
    // getters and setters omitted for brevity
}

public interface GatewayDao {
    GatewayEntity getGatewayEntity(Integer handlerId);
    GatewayEntity getFirstGatewayEntity();
}

public class GatewayImpl implements GatewayDao {
    private static Map<Integer, GatewayEntity> gatewayEntityMap = new HashMap<>();
    static {
        for (GatewayEnum e : GatewayEnum.values()) {
            gatewayEntityMap.put(e.getGatewayEntity().getHandlerId(), e.getGatewayEntity());
        }
    }
    @Override
    public GatewayEntity getGatewayEntity(Integer handlerId) { return gatewayEntityMap.get(handlerId); }
    @Override
    public GatewayEntity getFirstGatewayEntity() {
        for (GatewayEntity v : gatewayEntityMap.values()) {
            if (v.getPreHandlerId() == null) return v;
        }
        return null;
    }
}

public class GatewayHandlerEnumFactory {
    private static GatewayDao gatewayDao = new GatewayImpl();
    public static GatewayHandler getFirstGatewayHandler() {
        GatewayEntity firstEntity = gatewayDao.getFirstGatewayEntity();
        GatewayHandler firstHandler = newGatewayHandler(firstEntity);
        if (firstHandler == null) return null;
        GatewayEntity tempEntity = firstEntity;
        GatewayHandler tempHandler = firstHandler;
        Integer nextId;
        while ((nextId = tempEntity.getNextHandlerId()) != null) {
            GatewayEntity entity = gatewayDao.getGatewayEntity(nextId);
            GatewayHandler handler = newGatewayHandler(entity);
            tempHandler.setNext(handler);
            tempHandler = handler;
            tempEntity = entity;
        }
        return firstHandler;
    }
    private static GatewayHandler newGatewayHandler(GatewayEntity entity) {
        try {
            Class<?> clazz = Class.forName(entity.getConference());
            return (GatewayHandler) clazz.newInstance();
        } catch (Exception e) { e.printStackTrace(); }
        return null;
    }
}

public class GetewayClient {
    public static void main(String[] args) {
        GatewayHandler first = GatewayHandlerEnumFactory.getFirstGatewayHandler();
        first.service();
    }
}

In conclusion, the Chain of Responsibility pattern helps decouple request handling logic, improves extensibility, and can be dynamically configured via enums or external configuration. However, careful design is needed to avoid tight coupling between concrete handlers and to keep the chain maintainable.

Design patterns are an art; mastering them brings great benefits to software architecture.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Chain of ResponsibilityJava
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.