Behavioral Complexity: Medium

Chain of Responsibility Pattern

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.

The Problem

When processing an incoming order, you need to run multiple validation checks — inventory availability, payment method validity, fraud detection, and address verification. Hard-coding all of these checks into a single service creates a monolithic validator that is difficult to extend, reorder, or test in isolation.

The Solution

The Chain of Responsibility pattern organizes validators as a linked list of handler objects. Each handler performs its own check and then delegates to the next handler in the chain. All handlers run regardless of earlier failures, returning a complete list of validation errors.

Structure

  • Handler (abstract OrderValidator) — Declares validate() and abstract check(), holds a reference to the next handler via setNext().
  • Concrete HandlersInventoryCheckHandler, PaymentValidationHandler, FraudDetectionHandler, AddressValidationHandler.
  • Client (ValidationChainService) — Assembles the chain and invokes it.

Implementation

The implementation models an order validation pipeline with four sequential validators:

export interface ValidationResult {
  valid: boolean;
  errors: string[];
  handlerName: string;
}

export interface OrderData {
  items: { name: string; price: number; qty: number }[];
  paymentMethod: string;
  totalAmount: number;
  shippingAddress: {
    street: string;
    city: string;
    zip: string;
    country: string;
  };
}

export abstract class OrderValidator {
  private nextHandler: OrderValidator | null = null;

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

  async validate(order: OrderData): Promise<ValidationResult[]> {
    const result = await this.check(order);
    const results = [result];

    if (this.nextHandler) {
      const nextResults = await this.nextHandler.validate(order);
      results.push(...nextResults);
    }

    return results;
  }

  protected abstract check(order: OrderData): Promise<ValidationResult>;
}

NestJS Integration

Each handler is decorated with @Injectable() and registered as a provider. The ValidationChainService receives all handlers via constructor injection and assembles the chain in its constructor by calling setNext() on each handler. This leverages NestJS’s DI to manage handler lifecycle while keeping the chain assembly logic centralized.

When to Use

  • Multiple objects may handle a request and the handler isn’t known upfront.
  • You want to decouple senders from receivers.
  • The set of handlers and their order should be configurable at runtime.

When NOT to Use

  • There is only one handler — a direct call is simpler.
  • The chain is so long that debugging becomes difficult.