Back to blog

Design Patterns 101: Understanding Chain of Responsibility Pattern in TypeScript

4 min read
design-patterns typescript software-architecture clean-code
Share this post: Twitter LinkedIn

The Chain of Responsibility (CoR) is a behavioral design pattern that enables objects to forward requests along a chain of handlers. This approach decouples senders from receivers while providing flexibility in handling various request types.

The fundamental concept: “Each handler in the chain decides either to process the request or pass it to the next handler in the chain.”

Understanding the Basics

Participants in the Pattern

  1. Handler Interface/Abstract Class — Establishes the contract for handling requests
  2. Concrete Handlers — Implement the actual processing logic and determine whether to handle or forward requests
  3. Client — Initiates the request and starts the chain flow

Implementation in TypeScript

Setting Up the Handlers

Let’s start by creating a base handler interface and abstract class. Each concrete handler extends this base and implements its own handling logic:

interface Handler {
    setNext(handler: Handler): Handler;
    handleRequest(request: string): string | null;
}

abstract class AbstractHandler implements Handler {
    private nextHandler: Handler | null = null;

    public setNext(handler: Handler): Handler {
        this.nextHandler = handler;
        return handler;
    }

    public handleRequest(request: string): string | null {
        if (this.nextHandler) {
            return this.nextHandler.handleRequest(request);
        }
        return null;
    }
}

Creating Concrete Handlers

Now let’s implement specific handlers that process different types of requests:

class ConcreteHandlerA extends AbstractHandler {
    public handleRequest(request: string): string | null {
        if (request === 'A') {
            return `Handler A is processing the request: ${request}`;
        }
        return super.handleRequest(request);
    }
}

class ConcreteHandlerB extends AbstractHandler {
    public handleRequest(request: string): string | null {
        if (request === 'B') {
            return `Handler B is processing the request: ${request}`;
        }
        return super.handleRequest(request);
    }
}

Using the Chain

Here’s how to wire up the chain and process requests:

const handlerA = new ConcreteHandlerA();
const handlerB = new ConcreteHandlerB();

// Build the chain
handlerA.setNext(handlerB);

// Client initiates requests
const resultA = handlerA.handleRequest('A');
// Output: "Handler A is processing the request: A"

const resultB = handlerA.handleRequest('B');
// Output: "Handler B is processing the request: B"

const resultC = handlerA.handleRequest('C');
// Output: null (request not handled by any handler)

Real-World Use Cases

The Chain of Responsibility pattern is particularly useful in scenarios requiring flexible request handling:

1. Multi-Step Form Submissions

class ValidationHandler extends AbstractHandler {
    handleRequest(formData: FormData): FormData | null {
        if (this.isValid(formData)) {
            return super.handleRequest(formData);
        }
        throw new Error("Validation failed");
    }
}

class SanitizationHandler extends AbstractHandler {
    handleRequest(formData: FormData): FormData | null {
        const sanitized = this.sanitize(formData);
        return super.handleRequest(sanitized);
    }
}

class SubmissionHandler extends AbstractHandler {
    handleRequest(formData: FormData): FormData | null {
        this.submitToDatabase(formData);
        return formData;
    }
}

2. Authentication/Authorization Workflows

class AuthenticationHandler extends AbstractHandler {
    handleRequest(request: Request): Response | null {
        if (this.isAuthenticated(request)) {
            return super.handleRequest(request);
        }
        return new Response("Unauthorized", { status: 401 });
    }
}

class AuthorizationHandler extends AbstractHandler {
    handleRequest(request: Request): Response | null {
        if (this.isAuthorized(request)) {
            return super.handleRequest(request);
        }
        return new Response("Forbidden", { status: 403 });
    }
}

3. Data Processing Pipelines

class DataFetchHandler extends AbstractHandler {
    handleRequest(query: Query): Data | null {
        const data = this.fetchFromDatabase(query);
        return super.handleRequest(data);
    }
}

class DataTransformHandler extends AbstractHandler {
    handleRequest(data: Data): Data | null {
        const transformed = this.transform(data);
        return super.handleRequest(transformed);
    }
}

class DataCacheHandler extends AbstractHandler {
    handleRequest(data: Data): Data | null {
        this.cacheData(data);
        return data;
    }
}

Benefits

  1. Decoupling — Sender doesn’t need to know which handler will process the request
  2. Flexibility — Easy to add or remove handlers from the chain
  3. Single Responsibility — Each handler focuses on one specific task
  4. Open/Closed Principle — Add new handlers without modifying existing code

Considerations

  • Performance — Long chains can impact performance
  • Debugging — Tracing the flow through multiple handlers can be complex
  • Guaranteed Handling — No guarantee a request will be handled (might reach end of chain)

Conclusion

The Chain of Responsibility pattern facilitates decoupling request senders from receivers. It’s an excellent choice for scenarios requiring:

  • Multi-step processing
  • Flexible request routing
  • Conditional handling based on request type

By understanding and implementing this pattern, you can create more maintainable and flexible systems that adapt easily to changing requirements.


Next in the series: Strategy Pattern - choosing algorithms at runtime

Have questions about implementing Chain of Responsibility in your project? Let’s discuss in the comments!

Abisoye Alli-Balogun

Written by Abisoye Alli-Balogun

Full Stack Product Engineer building scalable distributed systems and high-performance applications. Passionate about microservices, cloud architecture, and creating delightful user experiences.

Enjoyed this post? Check out more articles on my blog.

View all posts

Comments