+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 295 of 355

๐Ÿ“˜ Chain of Responsibility: Request Handling

Master chain of responsibility: request handling in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐Ÿ“˜ Chain of Responsibility: Request Handling

๐ŸŽฏ Introduction

Ever been to a help desk where your request gets passed from one person to another until someone can help? Thatโ€™s the Chain of Responsibility pattern in action! ๐ŸŽญ In this tutorial, weโ€™ll explore how to implement this powerful pattern in TypeScript to create flexible, maintainable request handling systems.

Imagine building a customer support system where different types of requests need different handlers - some can be handled by bots ๐Ÿค–, others need human agents ๐Ÿ‘ค, and critical issues go straight to supervisors ๐Ÿ‘จโ€๐Ÿ’ผ. The Chain of Responsibility pattern makes this elegantly simple!

By the end of this tutorial, youโ€™ll be creating request handling chains that would make any support team jealous! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Chain of Responsibility

The Chain of Responsibility pattern is like a game of โ€œhot potatoโ€ ๐Ÿฅ” where each player (handler) decides whether to handle the potato (request) or pass it to the next player. Each handler in the chain has two choices:

  • Handle the request and stop the chain โœ‹
  • Pass it to the next handler in line โžก๏ธ

Think of it as a series of filters or checkpoints. Each checkpoint examines the request and decides: โ€œIs this for me? If yes, Iโ€™ll handle it. If not, next please!โ€ ๐ŸŽซ

In TypeScript, we create a chain of handler objects, each knowing about the next handler. When a request comes in, it travels through the chain until someone handles it or it reaches the end.

๐Ÿ”ง Basic Syntax and Usage

Letโ€™s start with a simple example - a support ticket system! ๐ŸŽซ

// ๐ŸŽฏ Define our request type
interface SupportTicket {
  id: string;
  priority: 'low' | 'medium' | 'high' | 'critical';
  issue: string;
  customerType: 'regular' | 'premium' | 'vip';
}

// ๐Ÿ—๏ธ Abstract handler class
abstract class SupportHandler {
  private nextHandler: SupportHandler | null = null;

  // ๐Ÿ”— Set the next handler in chain
  setNext(handler: SupportHandler): SupportHandler {
    this.nextHandler = handler;
    return handler; // ๐Ÿ’ก Return for chaining!
  }

  // ๐Ÿ“จ Handle the request
  handle(ticket: SupportTicket): string {
    const result = this.processTicket(ticket);
    
    if (result) {
      return result;
    }
    
    if (this.nextHandler) {
      return this.nextHandler.handle(ticket);
    }
    
    return "โš ๏ธ No handler available for this ticket!";
  }

  // ๐ŸŽฏ Each handler implements this
  protected abstract processTicket(ticket: SupportTicket): string | null;
}

// ๐Ÿค– Bot handler for simple issues
class BotHandler extends SupportHandler {
  protected processTicket(ticket: SupportTicket): string | null {
    if (ticket.priority === 'low') {
      return `๐Ÿค– Bot handled ticket ${ticket.id}: ${ticket.issue}`;
    }
    return null; // ๐Ÿ‘‹ Pass to next handler
  }
}

// ๐Ÿ‘ค Human agent handler
class AgentHandler extends SupportHandler {
  protected processTicket(ticket: SupportTicket): string | null {
    if (ticket.priority === 'medium' || ticket.priority === 'high') {
      return `๐Ÿ‘ค Agent handled ticket ${ticket.id}: ${ticket.issue}`;
    }
    return null;
  }
}

// ๐Ÿ‘จโ€๐Ÿ’ผ Supervisor handler for critical issues
class SupervisorHandler extends SupportHandler {
  protected processTicket(ticket: SupportTicket): string | null {
    if (ticket.priority === 'critical' || ticket.customerType === 'vip') {
      return `๐Ÿ‘จโ€๐Ÿ’ผ Supervisor handled ticket ${ticket.id}: ${ticket.issue}`;
    }
    return null;
  }
}

๐Ÿ’ก Practical Examples

Example 1: Customer Support System ๐ŸŽง

Letโ€™s build a complete support system with different handling strategies!

// ๐ŸŽฏ Enhanced ticket system with more features
interface EnhancedTicket extends SupportTicket {
  category: 'billing' | 'technical' | 'general' | 'complaint';
  responseTime: number; // in minutes
  resolved: boolean;
}

// ๐Ÿ’ฐ Billing specialist handler
class BillingHandler extends SupportHandler {
  protected processTicket(ticket: EnhancedTicket): string | null {
    if (ticket.category === 'billing') {
      console.log(`๐Ÿ’ฐ Processing billing issue...`);
      return `๐Ÿ’ณ Billing team resolved: ${ticket.issue}`;
    }
    return null;
  }
}

// ๐Ÿ”ง Technical support handler
class TechnicalHandler extends SupportHandler {
  private readonly expertise = ['login', 'performance', 'bug', 'crash'];
  
  protected processTicket(ticket: EnhancedTicket): string | null {
    if (ticket.category === 'technical') {
      const canHandle = this.expertise.some(keyword => 
        ticket.issue.toLowerCase().includes(keyword)
      );
      
      if (canHandle) {
        return `๐Ÿ”ง Tech support fixed: ${ticket.issue}`;
      }
    }
    return null;
  }
}

// ๐Ÿšจ Escalation handler for complaints
class EscalationHandler extends SupportHandler {
  protected processTicket(ticket: EnhancedTicket): string | null {
    if (ticket.category === 'complaint' || 
        ticket.responseTime > 60 ||
        ticket.customerType === 'vip') {
      return `๐Ÿšจ Escalated to management: ${ticket.issue}`;
    }
    return null;
  }
}

// ๐Ÿ—๏ธ Build the support chain
const buildSupportChain = (): SupportHandler => {
  const bot = new BotHandler();
  const billing = new BillingHandler();
  const tech = new TechnicalHandler();
  const agent = new AgentHandler();
  const escalation = new EscalationHandler();
  const supervisor = new SupervisorHandler();
  
  // ๐Ÿ”— Chain them together!
  bot.setNext(billing)
     .setNext(tech)
     .setNext(agent)
     .setNext(escalation)
     .setNext(supervisor);
  
  return bot; // ๐ŸŽฏ Return first handler
};

// ๐ŸŽฎ Let's test it!
const supportChain = buildSupportChain();

const tickets: EnhancedTicket[] = [
  {
    id: "T001",
    priority: 'low',
    issue: "How do I reset my password?",
    customerType: 'regular',
    category: 'general',
    responseTime: 5,
    resolved: false
  },
  {
    id: "T002",
    priority: 'high',
    issue: "Billing error on my account",
    customerType: 'premium',
    category: 'billing',
    responseTime: 15,
    resolved: false
  },
  {
    id: "T003",
    priority: 'critical',
    issue: "System crash losing customer data!",
    customerType: 'regular',
    category: 'technical',
    responseTime: 120,
    resolved: false
  }
];

tickets.forEach(ticket => {
  console.log(supportChain.handle(ticket));
});

Example 2: Authentication Chain ๐Ÿ”

Letโ€™s create a security chain for authentication!

// ๐Ÿ” Authentication request
interface AuthRequest {
  username: string;
  password?: string;
  token?: string;
  biometric?: string;
  ipAddress: string;
  deviceId: string;
}

interface AuthResult {
  success: boolean;
  method: string;
  message: string;
  userId?: string;
}

// ๐Ÿ—๏ธ Authentication handler base
abstract class AuthHandler {
  private next: AuthHandler | null = null;
  
  setNext(handler: AuthHandler): AuthHandler {
    this.next = handler;
    return handler;
  }
  
  async authenticate(request: AuthRequest): Promise<AuthResult> {
    const result = await this.tryAuthenticate(request);
    
    if (result.success) {
      return result;
    }
    
    if (this.next) {
      return this.next.authenticate(request);
    }
    
    return {
      success: false,
      method: 'none',
      message: 'โŒ All authentication methods failed'
    };
  }
  
  protected abstract tryAuthenticate(request: AuthRequest): Promise<AuthResult>;
}

// ๐Ÿ”‘ Password authentication
class PasswordAuthHandler extends AuthHandler {
  private users = new Map([
    ['alice', { password: 'secure123', id: 'U001' }],
    ['bob', { password: 'secret456', id: 'U002' }]
  ]);
  
  protected async tryAuthenticate(request: AuthRequest): Promise<AuthResult> {
    if (!request.password) {
      return { success: false, method: 'password', message: 'No password provided' };
    }
    
    const user = this.users.get(request.username);
    if (user && user.password === request.password) {
      console.log(`๐Ÿ”‘ Password auth successful for ${request.username}`);
      return {
        success: true,
        method: 'password',
        message: 'โœ… Password authentication successful',
        userId: user.id
      };
    }
    
    return { success: false, method: 'password', message: 'Invalid credentials' };
  }
}

// ๐ŸŽซ Token authentication
class TokenAuthHandler extends AuthHandler {
  private validTokens = new Map([
    ['TOKEN-123', { userId: 'U001', expires: Date.now() + 3600000 }],
    ['TOKEN-456', { userId: 'U002', expires: Date.now() + 3600000 }]
  ]);
  
  protected async tryAuthenticate(request: AuthRequest): Promise<AuthResult> {
    if (!request.token) {
      return { success: false, method: 'token', message: 'No token provided' };
    }
    
    const tokenData = this.validTokens.get(request.token);
    if (tokenData && tokenData.expires > Date.now()) {
      console.log(`๐ŸŽซ Token auth successful`);
      return {
        success: true,
        method: 'token',
        message: 'โœ… Token authentication successful',
        userId: tokenData.userId
      };
    }
    
    return { success: false, method: 'token', message: 'Invalid or expired token' };
  }
}

// ๐Ÿ‘† Biometric authentication
class BiometricAuthHandler extends AuthHandler {
  private biometricData = new Map([
    ['FINGER-001', 'U001'],
    ['FACE-002', 'U002']
  ]);
  
  protected async tryAuthenticate(request: AuthRequest): Promise<AuthResult> {
    if (!request.biometric) {
      return { success: false, method: 'biometric', message: 'No biometric data' };
    }
    
    const userId = this.biometricData.get(request.biometric);
    if (userId) {
      console.log(`๐Ÿ‘† Biometric auth successful`);
      return {
        success: true,
        method: 'biometric',
        message: 'โœ… Biometric authentication successful',
        userId
      };
    }
    
    return { success: false, method: 'biometric', message: 'Biometric not recognized' };
  }
}

// ๐ŸŒ IP-based authentication (for trusted networks)
class IPAuthHandler extends AuthHandler {
  private trustedIPs = ['192.168.1.100', '10.0.0.50'];
  
  protected async tryAuthenticate(request: AuthRequest): Promise<AuthResult> {
    if (this.trustedIPs.includes(request.ipAddress)) {
      console.log(`๐ŸŒ Trusted IP authentication`);
      return {
        success: true,
        method: 'ip',
        message: 'โœ… Trusted IP authentication',
        userId: 'guest'
      };
    }
    
    return { success: false, method: 'ip', message: 'Untrusted IP address' };
  }
}

Example 3: Request Validation Pipeline ๐Ÿšฆ

Letโ€™s build a validation chain for API requests!

// ๐Ÿ“ฆ API Request type
interface APIRequest {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  path: string;
  headers: Record<string, string>;
  body?: unknown;
  apiKey?: string;
  timestamp: number;
}

interface ValidationResult {
  valid: boolean;
  error?: string;
  processedBy: string;
}

// ๐Ÿ—๏ธ Validation handler base
abstract class ValidationHandler {
  private next: ValidationHandler | null = null;
  
  setNext(handler: ValidationHandler): ValidationHandler {
    this.next = handler;
    return handler;
  }
  
  validate(request: APIRequest): ValidationResult {
    const result = this.performValidation(request);
    
    if (!result.valid) {
      return result; // ๐Ÿ›‘ Stop on first failure
    }
    
    if (this.next) {
      return this.next.validate(request);
    }
    
    return { valid: true, processedBy: 'all' };
  }
  
  protected abstract performValidation(request: APIRequest): ValidationResult;
}

// ๐Ÿ”‘ API Key validator
class APIKeyValidator extends ValidationHandler {
  private validKeys = new Set(['KEY-123', 'KEY-456', 'KEY-789']);
  
  protected performValidation(request: APIRequest): ValidationResult {
    if (!request.apiKey) {
      return {
        valid: false,
        error: '๐Ÿ”‘ API key required',
        processedBy: 'APIKeyValidator'
      };
    }
    
    if (!this.validKeys.has(request.apiKey)) {
      return {
        valid: false,
        error: 'โŒ Invalid API key',
        processedBy: 'APIKeyValidator'
      };
    }
    
    console.log('โœ… API key validated');
    return { valid: true, processedBy: 'APIKeyValidator' };
  }
}

// โฐ Rate limit validator
class RateLimitValidator extends ValidationHandler {
  private requests = new Map<string, number[]>();
  private readonly limit = 10; // 10 requests per minute
  
  protected performValidation(request: APIRequest): ValidationResult {
    const key = request.apiKey || 'anonymous';
    const now = Date.now();
    const minute = 60000;
    
    // ๐Ÿงน Clean old requests
    const userRequests = this.requests.get(key) || [];
    const recentRequests = userRequests.filter(time => now - time < minute);
    
    if (recentRequests.length >= this.limit) {
      return {
        valid: false,
        error: `โฐ Rate limit exceeded (${this.limit}/min)`,
        processedBy: 'RateLimitValidator'
      };
    }
    
    recentRequests.push(now);
    this.requests.set(key, recentRequests);
    
    console.log(`โœ… Rate limit OK (${recentRequests.length}/${this.limit})`);
    return { valid: true, processedBy: 'RateLimitValidator' };
  }
}

// ๐Ÿ“ Content validator
class ContentValidator extends ValidationHandler {
  protected performValidation(request: APIRequest): ValidationResult {
    // ๐Ÿšซ Check content type for POST/PUT
    if (['POST', 'PUT'].includes(request.method)) {
      if (!request.headers['content-type']) {
        return {
          valid: false,
          error: '๐Ÿ“ Content-Type header required',
          processedBy: 'ContentValidator'
        };
      }
      
      if (!request.body) {
        return {
          valid: false,
          error: '๐Ÿ“ฆ Request body required',
          processedBy: 'ContentValidator'
        };
      }
    }
    
    // ๐Ÿ” Validate JSON body
    if (request.headers['content-type']?.includes('application/json')) {
      try {
        if (typeof request.body === 'string') {
          JSON.parse(request.body);
        }
      } catch {
        return {
          valid: false,
          error: 'โŒ Invalid JSON body',
          processedBy: 'ContentValidator'
        };
      }
    }
    
    console.log('โœ… Content validated');
    return { valid: true, processedBy: 'ContentValidator' };
  }
}

// ๐Ÿ›ก๏ธ Security validator
class SecurityValidator extends ValidationHandler {
  private blockedPaths = ['/admin', '/internal', '/.env'];
  private sqlPatterns = /(\bselect\b|\binsert\b|\bdelete\b|\bdrop\b)/i;
  
  protected performValidation(request: APIRequest): ValidationResult {
    // ๐Ÿšซ Check blocked paths
    if (this.blockedPaths.some(path => request.path.includes(path))) {
      return {
        valid: false,
        error: '๐Ÿ›ก๏ธ Access denied to restricted path',
        processedBy: 'SecurityValidator'
      };
    }
    
    // ๐Ÿ” Check for SQL injection attempts
    const checkString = `${request.path} ${JSON.stringify(request.body || '')}`;
    if (this.sqlPatterns.test(checkString)) {
      return {
        valid: false,
        error: 'โš ๏ธ Potential SQL injection detected',
        processedBy: 'SecurityValidator'
      };
    }
    
    console.log('โœ… Security check passed');
    return { valid: true, processedBy: 'SecurityValidator' };
  }
}

// ๐ŸŽฎ Test the validation chain!
const buildValidationChain = (): ValidationHandler => {
  const apiKey = new APIKeyValidator();
  const rateLimit = new RateLimitValidator();
  const content = new ContentValidator();
  const security = new SecurityValidator();
  
  apiKey.setNext(rateLimit)
        .setNext(security)
        .setNext(content);
  
  return apiKey;
};

const validator = buildValidationChain();

// ๐Ÿงช Test various requests
const testRequests: APIRequest[] = [
  {
    method: 'GET',
    path: '/api/users',
    headers: {},
    timestamp: Date.now()
  },
  {
    method: 'POST',
    path: '/api/users',
    headers: { 'content-type': 'application/json' },
    body: { name: 'Alice ๐ŸŽ‰' },
    apiKey: 'KEY-123',
    timestamp: Date.now()
  },
  {
    method: 'DELETE',
    path: '/admin/users',
    headers: {},
    apiKey: 'KEY-456',
    timestamp: Date.now()
  }
];

testRequests.forEach((req, i) => {
  console.log(`\n๐Ÿ” Testing request ${i + 1}:`);
  const result = validator.validate(req);
  console.log(result);
});

๐Ÿš€ Advanced Concepts

Dynamic Chain Building ๐Ÿ—๏ธ

Letโ€™s create a system that builds chains dynamically based on configuration!

// ๐ŸŽฏ Generic handler with priority
interface HandlerConfig {
  name: string;
  priority: number;
  enabled: boolean;
  conditions?: Record<string, unknown>;
}

// ๐Ÿญ Handler factory
class HandlerFactory<T> {
  private handlers = new Map<string, new() => Handler<T>>();
  
  register(name: string, HandlerClass: new() => Handler<T>): void {
    this.handlers.set(name, HandlerClass);
  }
  
  create(name: string): Handler<T> | null {
    const HandlerClass = this.handlers.get(name);
    return HandlerClass ? new HandlerClass() : null;
  }
}

// ๐Ÿ”— Advanced handler with middleware support
abstract class Handler<T> {
  private next: Handler<T> | null = null;
  private middlewares: ((data: T) => T)[] = [];
  
  setNext(handler: Handler<T>): Handler<T> {
    this.next = handler;
    return handler;
  }
  
  use(middleware: (data: T) => T): this {
    this.middlewares.push(middleware);
    return this;
  }
  
  async handle(data: T): Promise<T | null> {
    // ๐ŸŽฏ Apply middlewares
    let processedData = data;
    for (const middleware of this.middlewares) {
      processedData = middleware(processedData);
    }
    
    const result = await this.process(processedData);
    
    if (result !== null) {
      return result;
    }
    
    return this.next ? this.next.handle(data) : null;
  }
  
  protected abstract process(data: T): Promise<T | null>;
}

// ๐ŸŽฏ Chain builder with configuration
class ChainBuilder<T> {
  private factory: HandlerFactory<T>;
  
  constructor(factory: HandlerFactory<T>) {
    this.factory = factory;
  }
  
  build(configs: HandlerConfig[]): Handler<T> | null {
    // ๐Ÿ”ข Sort by priority
    const sorted = configs
      .filter(c => c.enabled)
      .sort((a, b) => a.priority - b.priority);
    
    if (sorted.length === 0) return null;
    
    // ๐Ÿ—๏ธ Build the chain
    const handlers = sorted
      .map(config => this.factory.create(config.name))
      .filter((h): h is Handler<T> => h !== null);
    
    if (handlers.length === 0) return null;
    
    // ๐Ÿ”— Link them together
    for (let i = 0; i < handlers.length - 1; i++) {
      handlers[i].setNext(handlers[i + 1]);
    }
    
    return handlers[0];
  }
}

// ๐Ÿ“Š Example: Order processing chain
interface Order {
  id: string;
  items: Array<{ name: string; price: number; emoji: string }>;
  total: number;
  status: string;
  customer: {
    name: string;
    membership: 'bronze' | 'silver' | 'gold';
    previousOrders: number;
  };
}

// ๐Ÿ’ฐ Discount handler
class DiscountHandler extends Handler<Order> {
  protected async process(order: Order): Promise<Order | null> {
    if (order.customer.membership === 'gold') {
      const discount = order.total * 0.2;
      console.log(`๐Ÿ’ฐ Applied 20% gold discount: -$${discount.toFixed(2)}`);
      return {
        ...order,
        total: order.total - discount,
        status: 'discounted'
      };
    }
    return null;
  }
}

// ๐ŸŽ Loyalty points handler
class LoyaltyHandler extends Handler<Order> {
  protected async process(order: Order): Promise<Order | null> {
    if (order.customer.previousOrders >= 5) {
      const points = Math.floor(order.total * 10);
      console.log(`๐ŸŽ Awarded ${points} loyalty points!`);
      return {
        ...order,
        status: 'loyalty_processed'
      };
    }
    return null;
  }
}

// ๐Ÿ“ฆ Inventory check handler
class InventoryHandler extends Handler<Order> {
  private inventory = new Map([
    ['Pizza ๐Ÿ•', 10],
    ['Burger ๐Ÿ”', 15],
    ['Taco ๐ŸŒฎ', 20]
  ]);
  
  protected async process(order: Order): Promise<Order | null> {
    for (const item of order.items) {
      const stock = this.inventory.get(item.name) || 0;
      if (stock < 1) {
        console.log(`โŒ Out of stock: ${item.name}`);
        return {
          ...order,
          status: 'out_of_stock'
        };
      }
    }
    
    console.log('โœ… All items in stock!');
    return null;
  }
}

// ๐Ÿšš Shipping handler
class ShippingHandler extends Handler<Order> {
  protected async process(order: Order): Promise<Order | null> {
    if (order.total > 50) {
      console.log('๐Ÿšš Free shipping applied!');
      return {
        ...order,
        status: 'free_shipping'
      };
    }
    return null;
  }
}

// ๐ŸŽฎ Let's use the advanced chain!
const setupOrderProcessing = () => {
  // ๐Ÿญ Setup factory
  const factory = new HandlerFactory<Order>();
  factory.register('discount', DiscountHandler);
  factory.register('loyalty', LoyaltyHandler);
  factory.register('inventory', InventoryHandler);
  factory.register('shipping', ShippingHandler);
  
  // ๐Ÿ“‹ Configuration
  const config: HandlerConfig[] = [
    { name: 'inventory', priority: 1, enabled: true },
    { name: 'discount', priority: 2, enabled: true },
    { name: 'shipping', priority: 3, enabled: true },
    { name: 'loyalty', priority: 4, enabled: true }
  ];
  
  // ๐Ÿ—๏ธ Build chain
  const builder = new ChainBuilder(factory);
  return builder.build(config);
};

// ๐Ÿงช Test the advanced chain
const orderChain = setupOrderProcessing();
if (orderChain) {
  const testOrder: Order = {
    id: 'ORD-001',
    items: [
      { name: 'Pizza ๐Ÿ•', price: 15, emoji: '๐Ÿ•' },
      { name: 'Burger ๐Ÿ”', price: 12, emoji: '๐Ÿ”' },
      { name: 'Taco ๐ŸŒฎ', price: 8, emoji: '๐ŸŒฎ' }
    ],
    total: 60,
    status: 'pending',
    customer: {
      name: 'Alice',
      membership: 'gold',
      previousOrders: 10
    }
  };
  
  orderChain.handle(testOrder).then(result => {
    console.log('๐Ÿ“Š Final order:', result);
  });
}

โš ๏ธ Common Pitfalls and Solutions

โŒ Wrong: Forgetting to set next handler

// โŒ Broken chain - handlers not connected!
const handler1 = new BotHandler();
const handler2 = new AgentHandler();
const handler3 = new SupervisorHandler();

// ๐Ÿ˜ฑ They're not linked!
const result = handler1.handle(ticket); // Only handler1 will work

โœ… Correct: Properly linking handlers

// โœ… Properly linked chain
const handler1 = new BotHandler();
const handler2 = new AgentHandler();
const handler3 = new SupervisorHandler();

// ๐Ÿ”— Link them together
handler1.setNext(handler2).setNext(handler3);

const result = handler1.handle(ticket); // Full chain works!

โŒ Wrong: Circular references

// โŒ Infinite loop danger!
const handlerA = new HandlerA();
const handlerB = new HandlerB();

handlerA.setNext(handlerB);
handlerB.setNext(handlerA); // ๐Ÿ˜ฑ Circular reference!

โœ… Correct: Linear chain with termination

// โœ… Safe linear chain
const handlers: Handler[] = [
  new HandlerA(),
  new HandlerB(),
  new HandlerC()
];

// ๐Ÿ”— Link in order
for (let i = 0; i < handlers.length - 1; i++) {
  handlers[i].setNext(handlers[i + 1]);
}
// Last handler has no next - chain ends safely

โŒ Wrong: Not handling null results

// โŒ Assumes handler always returns something
class BadHandler extends Handler {
  handle(request: Request): Response {
    const result = this.process(request);
    return result.data; // ๐Ÿ’ฅ What if result is null?
  }
}

โœ… Correct: Proper null handling

// โœ… Handles null results properly
class GoodHandler extends Handler {
  handle(request: Request): Response | null {
    const result = this.process(request);
    
    if (result) {
      return result;
    }
    
    // ๐Ÿ” Try next handler or return default
    return this.next ? this.next.handle(request) : null;
  }
}

๐Ÿ› ๏ธ Best Practices

  1. Keep handlers focused ๐ŸŽฏ

    • Each handler should have a single responsibility
    • Donโ€™t put too much logic in one handler
  2. Order matters ๐Ÿ“Š

    • Place more specific handlers before general ones
    • Consider performance - put fast checks first
  3. Make handlers configurable โš™๏ธ

    • Use constructor parameters for flexibility
    • Allow dynamic chain building
  4. Add logging ๐Ÿ“

    • Log which handler processed the request
    • Track performance metrics
  5. Handle errors gracefully ๐Ÿ›ก๏ธ

    • Wrap handler logic in try-catch
    • Decide whether errors stop the chain
  6. Use TypeScript features ๐Ÿ’Ž

    • Leverage generics for reusable handlers
    • Use abstract classes for common behavior
  7. Test each handler independently ๐Ÿงช

    • Unit test individual handlers
    • Integration test the full chain
  8. Document the chain flow ๐Ÿ“š

    • Create diagrams showing handler order
    • Document what each handler handles

๐Ÿงช Hands-On Exercise

Time to build your own chain! Create a notification system that sends alerts through different channels based on priority and user preferences.

Your Challenge:

  1. Create handlers for Email, SMS, Push, and Slack notifications
  2. Implement priority-based routing (critical โ†’ all channels, high โ†’ SMS+Email, etc.)
  3. Add user preference checking (some users might opt-out of certain channels)
  4. Include retry logic for failed notifications
๐Ÿ’ก Click to see the solution
// ๐Ÿ“ฑ Notification system implementation
interface Notification {
  id: string;
  message: string;
  priority: 'low' | 'medium' | 'high' | 'critical';
  userId: string;
  timestamp: number;
  channels?: string[];
}

interface UserPreferences {
  userId: string;
  enabledChannels: Set<string>;
  quietHours?: { start: number; end: number };
}

interface NotificationResult {
  sent: boolean;
  channel: string;
  error?: string;
}

// ๐Ÿ—๏ธ Base notification handler
abstract class NotificationHandler {
  private next: NotificationHandler | null = null;
  protected preferences = new Map<string, UserPreferences>([
    ['user1', { 
      userId: 'user1', 
      enabledChannels: new Set(['email', 'push', 'slack']),
      quietHours: { start: 22, end: 8 }
    }],
    ['user2', { 
      userId: 'user2', 
      enabledChannels: new Set(['email', 'sms']) 
    }]
  ]);
  
  setNext(handler: NotificationHandler): NotificationHandler {
    this.next = handler;
    return handler;
  }
  
  async send(notification: Notification): Promise<NotificationResult[]> {
    const results: NotificationResult[] = [];
    
    // ๐Ÿ” Check if this handler should process
    if (this.shouldHandle(notification)) {
      const result = await this.sendNotification(notification);
      results.push(result);
      
      // ๐Ÿ›‘ Stop if critical and sent successfully
      if (notification.priority === 'critical' && result.sent) {
        return results;
      }
    }
    
    // ๐Ÿ“จ Pass to next handler
    if (this.next) {
      const nextResults = await this.next.send(notification);
      results.push(...nextResults);
    }
    
    return results;
  }
  
  protected abstract shouldHandle(notification: Notification): boolean;
  protected abstract sendNotification(notification: Notification): Promise<NotificationResult>;
  
  protected canSendToUser(userId: string, channel: string): boolean {
    const prefs = this.preferences.get(userId);
    if (!prefs) return true; // Default to enabled
    
    // ๐Ÿ”• Check quiet hours
    if (prefs.quietHours) {
      const hour = new Date().getHours();
      if (hour >= prefs.quietHours.start || hour < prefs.quietHours.end) {
        console.log(`๐Ÿ”• User ${userId} is in quiet hours`);
        return false;
      }
    }
    
    return prefs.enabledChannels.has(channel);
  }
}

// ๐Ÿ“ง Email handler
class EmailHandler extends NotificationHandler {
  private retryCount = 3;
  
  protected shouldHandle(notification: Notification): boolean {
    return notification.priority !== 'low' &&
           this.canSendToUser(notification.userId, 'email');
  }
  
  protected async sendNotification(notification: Notification): Promise<NotificationResult> {
    let attempts = 0;
    
    while (attempts < this.retryCount) {
      try {
        // ๐ŸŽฏ Simulate sending email
        await this.simulateSend();
        console.log(`๐Ÿ“ง Email sent: ${notification.message}`);
        return { sent: true, channel: 'email' };
      } catch (error) {
        attempts++;
        console.log(`โŒ Email attempt ${attempts} failed`);
      }
    }
    
    return { sent: false, channel: 'email', error: 'Max retries exceeded' };
  }
  
  private async simulateSend(): Promise<void> {
    // 90% success rate
    if (Math.random() > 0.9) {
      throw new Error('Network timeout');
    }
  }
}

// ๐Ÿ“ฑ SMS handler
class SMSHandler extends NotificationHandler {
  protected shouldHandle(notification: Notification): boolean {
    return (notification.priority === 'high' || notification.priority === 'critical') &&
           this.canSendToUser(notification.userId, 'sms');
  }
  
  protected async sendNotification(notification: Notification): Promise<NotificationResult> {
    // ๐Ÿ“ Truncate message for SMS
    const truncated = notification.message.substring(0, 160);
    console.log(`๐Ÿ“ฑ SMS sent: ${truncated}`);
    return { sent: true, channel: 'sms' };
  }
}

// ๐Ÿ”” Push notification handler
class PushHandler extends NotificationHandler {
  protected shouldHandle(notification: Notification): boolean {
    return notification.priority !== 'low' &&
           this.canSendToUser(notification.userId, 'push');
  }
  
  protected async sendNotification(notification: Notification): Promise<NotificationResult> {
    console.log(`๐Ÿ”” Push notification sent: ${notification.message}`);
    return { sent: true, channel: 'push' };
  }
}

// ๐Ÿ’ฌ Slack handler
class SlackHandler extends NotificationHandler {
  protected shouldHandle(notification: Notification): boolean {
    return notification.priority === 'critical' &&
           this.canSendToUser(notification.userId, 'slack');
  }
  
  protected async sendNotification(notification: Notification): Promise<NotificationResult> {
    const formattedMessage = `๐Ÿšจ *Critical Alert*\n${notification.message}`;
    console.log(`๐Ÿ’ฌ Slack message sent: ${formattedMessage}`);
    return { sent: true, channel: 'slack' };
  }
}

// ๐Ÿ—๏ธ Build notification chain
const buildNotificationChain = (): NotificationHandler => {
  const email = new EmailHandler();
  const sms = new SMSHandler();
  const push = new PushHandler();
  const slack = new SlackHandler();
  
  email.setNext(sms).setNext(push).setNext(slack);
  
  return email;
};

// ๐Ÿงช Test the notification system
const testNotificationSystem = async () => {
  const chain = buildNotificationChain();
  
  const notifications: Notification[] = [
    {
      id: 'N001',
      message: 'Your order has been shipped! ๐Ÿ“ฆ',
      priority: 'low',
      userId: 'user1',
      timestamp: Date.now()
    },
    {
      id: 'N002',
      message: 'Payment reminder: $99 due tomorrow ๐Ÿ’ณ',
      priority: 'high',
      userId: 'user1',
      timestamp: Date.now()
    },
    {
      id: 'N003',
      message: 'SECURITY ALERT: Unusual login detected! ๐Ÿšจ',
      priority: 'critical',
      userId: 'user2',
      timestamp: Date.now()
    }
  ];
  
  for (const notification of notifications) {
    console.log(`\n๐Ÿ“จ Processing ${notification.priority} notification:`);
    const results = await chain.send(notification);
    console.log('Results:', results);
  }
};

// ๐ŸŽฎ Run the test!
testNotificationSystem();

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered the Chain of Responsibility pattern! Hereโ€™s what youโ€™ve learned:

  • ๐Ÿ”— Chain building: Link handlers together to create processing pipelines
  • ๐ŸŽฏ Single responsibility: Each handler focuses on one type of request
  • โžก๏ธ Request forwarding: Pass requests along until someone handles them
  • ๐Ÿ›ก๏ธ Decoupling: Senders donโ€™t need to know about specific handlers
  • ๐Ÿ”ง Flexibility: Easily add, remove, or reorder handlers
  • ๐ŸŽฎ Real-world uses: Support systems, authentication, validation, and more!

The Chain of Responsibility pattern is perfect when you need to:

  • Process requests through multiple steps
  • Allow different handlers for different scenarios
  • Build flexible, extensible systems
  • Decouple request senders from handlers

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve built some amazing chain-based systems! Your request handling skills are now top-notch!

To continue your journey:

  • Try combining chains with other patterns (like Strategy or Observer)
  • Build a middleware system for a web framework
  • Create a plugin architecture using chains
  • Implement an event processing pipeline

Keep building those chains, and remember - every great system is just a series of well-connected handlers! Youโ€™re doing fantastic! ๐ŸŒŸ

Happy coding! ๐Ÿš€โœจ