Java

Chain of Responsibility in Java with a Request Validation Pipeline

5 min read Updated Mar 27, 2026

Java Design Patterns Series

Chain of Responsibility becomes valuable when validation or processing logic needs a stable sequence of independently understandable steps.

It is not automatically better than one method. It is better when the sequence itself is part of the design.

Quick Summary

Question Strong fit Weak fit
Do steps need a clear order? yes no
Do new checks get inserted often? yes rarely
Can each handler own one narrow responsibility? yes no
Do handlers secretly depend on one another’s side effects? no yes

The pattern works when the chain is explicit and each step behaves like a good pipeline stage.

A Good Example: Order Request Validation

Suppose an order request must pass these checks:

  • caller is authenticated
  • cart is not empty
  • payment method is supported
  • shipping destination is serviceable

Those rules are easier to evolve when they are modular and ordered. The chain makes that order visible.

A Clean Java Shape

public abstract class ValidationHandler {
    private ValidationHandler next;

    public ValidationHandler linkWith(ValidationHandler next) {
        this.next = next;
        return next;
    }

    public final void handle(OrderRequest request) {
        validate(request);
        if (next != null) {
            next.handle(request);
        }
    }

    protected abstract void validate(OrderRequest request);
}

public final class AuthValidationHandler extends ValidationHandler {
    @Override
    protected void validate(OrderRequest request) {
        if (!request.isAuthenticated()) {
            throw new IllegalStateException("Authentication required");
        }
    }
}

public final class CartValidationHandler extends ValidationHandler {
    @Override
    protected void validate(OrderRequest request) {
        if (request.getItemCount() == 0) {
            throw new IllegalStateException("Cart is empty");
        }
    }
}

public final class PaymentValidationHandler extends ValidationHandler {
    @Override
    protected void validate(OrderRequest request) {
        if (!"CARD".equals(request.getPaymentMethod())) {
            throw new IllegalStateException("Unsupported payment method");
        }
    }
}

Assembly stays explicit:

ValidationHandler chain = new AuthValidationHandler();
chain.linkWith(new CartValidationHandler())
     .linkWith(new PaymentValidationHandler());

chain.handle(new OrderRequest(true, 3, "CARD"));

The UML view is useful here because it shows two things at once: shared handler structure and the explicit next link that creates the pipeline.

classDiagram
    class ValidationHandler {
      <<abstract>>
      -next
      +linkWith()
      +handle()
      #validate()
    }
    class AuthValidationHandler
    class CartValidationHandler
    class PaymentValidationHandler
    class OrderRequest

    ValidationHandler <|-- AuthValidationHandler
    ValidationHandler <|-- CartValidationHandler
    ValidationHandler <|-- PaymentValidationHandler
    ValidationHandler --> ValidationHandler : next
    AuthValidationHandler ..> OrderRequest : validates
    CartValidationHandler ..> OrderRequest : validates
    PaymentValidationHandler ..> OrderRequest : validates

That gives the team one visible flow and one clear insertion point for new rules.

Why Teams Like This Pattern

The gains are practical:

  • each rule stays small
  • ordering is visible
  • new handlers are easy to add
  • tests can focus on one step at a time

This is especially helpful when the rule set changes often because of compliance, fraud checks, or market-specific policies.

The Decision You Must Make Up Front

Before implementing the chain, decide whether it is:

Fail-fast

The first violation stops processing. This is simpler and often better for command workflows.

Error-collecting

Every handler contributes violations, and the caller receives a full list. This is often better for request validation APIs or form-style feedback.

Do not mix those styles accidentally. That is one of the fastest ways to make the chain confusing.

Where This Pattern Goes Wrong

Handlers mutate hidden shared state

If one handler enriches the request in a way later handlers secretly depend on, the pipeline becomes fragile.

Ordering rules are implicit

If nobody knows where the chain is assembled, debugging becomes guesswork.

Some handlers throw while others silently record errors

That creates inconsistent contract behavior.

One handler owns too much

A handler named FraudAndPolicyAndInventoryValidationHandler is a clue that the responsibilities were not actually separated.

Alternatives Worth Considering

One plain validator method

Best when the logic is short and unlikely to grow.

Specification pattern

Better when the real problem is composable rule logic rather than ordered processing.

Middleware or interceptor pipeline

Better when the steps are framework-level request processing concerns, not domain validation.

Chain of Responsibility is strongest when step ordering matters and the pipeline itself is a first-class design choice.

A Better Production Rule

Keep handlers either:

  • pure validators, or
  • clearly documented enrichers

Do not blur the two casually. That one boundary often determines whether the pattern stays readable.

Testing Strategy

Useful tests include:

  • each handler rejects invalid input independently
  • valid input flows through the whole chain
  • inserting a new handler does not require rewriting existing ones
  • chain order is correct for business semantics
  • fail-fast or collect-all behavior is enforced consistently

If changing one rule forces you to rewrite three handlers, the chain is probably not decoupled enough.

Key Takeaways

  • Chain of Responsibility is for ordered, modular processing steps.
  • It helps when the flow needs extension without rewriting a large validator.
  • It fails when handlers depend on hidden side effects or inconsistent contracts.
  • The most important design choice is not the class hierarchy; it is whether the chain behavior and assembly order are explicit.

Categories

Tags

Continue reading

Previous State Pattern in Java with an Order Lifecycle Example Next Choosing and Combining Design Patterns in Java

Comments