CrackedRuby CrackedRuby

Overview

Chain of Responsibility decouples request senders from receivers by giving multiple objects the opportunity to handle a request. The pattern chains receiving objects and passes the request along the chain until an object handles it. Each handler in the chain contains a reference to the next handler and decides whether to process the request or pass it forward.

The pattern emerged from the need to avoid coupling the sender of a request to its receiver, particularly when multiple objects might handle the request. Rather than the sender knowing which specific object should handle the request, the pattern allows the request to travel through a sequence of potential handlers.

Common applications include event handling systems, middleware architectures, logging frameworks, authentication pipelines, and validation chains. Web frameworks often use this pattern for request processing, where each middleware component examines the request and either handles it, modifies it, or passes it to the next component.

# Basic chain structure
class Handler
  attr_accessor :next_handler
  
  def handle(request)
    if can_handle?(request)
      process(request)
    elsif @next_handler
      @next_handler.handle(request)
    end
  end
end

The pattern consists of three primary components: the Handler interface defining the request handling method, Concrete Handlers that implement specific handling logic, and the Client that initiates requests into the chain. The chain forms at runtime, allowing dynamic configuration of the handling sequence.

Key Principles

The Chain of Responsibility operates on several fundamental principles that distinguish it from direct object communication patterns.

Single Responsibility Per Handler: Each handler in the chain addresses one specific concern or responsibility. A handler evaluates whether it can process the request based on its criteria, processes it if appropriate, and otherwise delegates to the next handler. This separation prevents handlers from containing complex conditional logic about all possible request types.

Dynamic Chain Construction: The chain assembles at runtime rather than compile time. Handlers connect through references to successor handlers, allowing the chain order and composition to change based on configuration or runtime conditions. This dynamic nature enables the same handlers to participate in different chains for different contexts.

Request Propagation Control: Each handler controls whether the request continues along the chain. A handler may process the request and stop propagation, process the request and continue propagation, or skip processing entirely and forward the request. This control mechanism differs from rigid pipelines where every stage must execute.

class BaseValidator
  attr_accessor :next_validator
  
  def validate(data)
    return false unless valid?(data)
    return true unless @next_validator
    @next_validator.validate(data)
  end
  
  def valid?(data)
    raise NotImplementedError
  end
end

class LengthValidator < BaseValidator
  def initialize(min_length)
    @min_length = min_length
  end
  
  def valid?(data)
    data.length >= @min_length
  end
end

class FormatValidator < BaseValidator
  def initialize(pattern)
    @pattern = pattern
  end
  
  def valid?(data)
    data.match?(@pattern)
  end
end

Loose Coupling Between Handlers: Handlers remain independent of other handlers in the chain. A handler knows only about the Handler interface, not about concrete handler implementations. This independence allows handlers to be reused in different chains and tested in isolation.

Implicit Receiver: The client sending the request does not specify which handler will ultimately process it. The chain determines the appropriate handler through the propagation mechanism. This implicit receiver selection simplifies client code and centralizes handling logic within the chain.

The pattern supports both homogeneous chains where all handlers share the same interface and heterogeneous chains where handlers may implement different interfaces but share a common propagation mechanism. Most implementations favor homogeneous chains for clarity and maintainability.

Termination Conditions: Chains require clear termination conditions. A handler processes the request and stops propagation, the request reaches the end of the chain without being handled, or an error condition interrupts the chain. Proper termination prevents infinite loops and ensures predictable behavior.

class AuthenticationChain
  def initialize
    @handlers = []
  end
  
  def add_handler(handler)
    @handlers << handler
    link_handlers
    self
  end
  
  def authenticate(credentials)
    return false if @handlers.empty?
    @handlers.first.handle(credentials)
  end
  
  private
  
  def link_handlers
    @handlers.each_cons(2) do |current, next_handler|
      current.next_handler = next_handler
    end
  end
end

Design Considerations

Chain of Responsibility suits scenarios where multiple objects might handle a request, the appropriate handler should be determined automatically, and the set of handlers should be specified dynamically.

When to Apply Chain of Responsibility: Use this pattern when the handling logic naturally decomposes into sequential steps with different responsibilities. The pattern excels in request processing pipelines, validation sequences, event handling hierarchies, and middleware stacks. Consider the pattern when handlers may change frequently or when the handler selection logic would otherwise clutter client code.

The pattern addresses scenarios where direct coupling between sender and receiver creates inflexibility. In systems where new request types or handling strategies appear regularly, the chain provides extension points without modifying existing code.

Trade-offs and Limitations: Chain of Responsibility introduces several trade-offs. Request handling becomes less explicit since the client does not specify the handler directly. Debugging can prove challenging when determining which handler processed a request or why a request was not handled. Performance may degrade with long chains since the request potentially traverses many handlers.

The pattern may result in requests passing through the entire chain without being handled. Applications must address unhandled requests explicitly, either through a default handler at the chain's end or through error handling when no handler processes the request.

class RequestProcessor
  def initialize
    @chain = build_chain
  end
  
  def process(request)
    result = @chain.handle(request)
    raise UnhandledRequestError, "No handler processed #{request.type}" if result.nil?
    result
  end
  
  private
  
  def build_chain
    # Chain construction with specific handler order
    cache = CacheHandler.new
    auth = AuthenticationHandler.new
    rate_limit = RateLimitHandler.new
    business = BusinessLogicHandler.new
    
    cache.next_handler = auth
    auth.next_handler = rate_limit
    rate_limit.next_handler = business
    
    cache
  end
end

Comparison with Alternative Patterns: Chain of Responsibility shares similarities with other behavioral patterns but serves distinct purposes. The Command pattern encapsulates requests as objects but does not inherently support sequential processing. The Strategy pattern selects one algorithm from a family but does not chain operations. The Decorator pattern adds responsibilities to objects but focuses on enhancement rather than conditional handling.

Composite pattern shares structural similarities with Chain of Responsibility in that both involve recursive structures. However, Composite focuses on part-whole hierarchies while Chain of Responsibility emphasizes request delegation.

Handler Ordering Considerations: The sequence of handlers in the chain significantly impacts behavior. Handlers that filter or reject requests should typically appear early in the chain to avoid unnecessary processing. Handlers with side effects should be positioned carefully to ensure they execute only when appropriate. Performance-critical handlers may be placed early to optimize common cases.

Some chains benefit from dynamic reordering based on handler effectiveness or request patterns. Adaptive chains measure handler performance and adjust ordering to optimize throughput.

Chain Composition Strategies: Chains can be composed statically at initialization, built dynamically based on request properties, or configured through external configuration. Static composition provides predictability and performance. Dynamic composition offers flexibility for complex routing logic. Configuration-based composition separates chain definition from code, enabling runtime adjustments.

class DynamicChain
  def initialize
    @handler_registry = {}
  end
  
  def register_handler(name, handler)
    @handler_registry[name] = handler
  end
  
  def build_chain(handler_names)
    handlers = handler_names.map { |name| @handler_registry[name] }
    handlers.each_cons(2) do |current, next_handler|
      current.next_handler = next_handler
    end
    handlers.first
  end
end

Ruby Implementation

Ruby's dynamic nature and object model provide several approaches to implementing Chain of Responsibility. The pattern integrates naturally with Ruby idioms including modules, procs, and method chaining.

Class-Based Implementation: The standard implementation uses classes for handlers with explicit next handler references.

class Handler
  attr_accessor :next_handler
  
  def handle(request)
    if can_handle?(request)
      process(request)
    elsif @next_handler
      @next_handler.handle(request)
    else
      nil # Request not handled
    end
  end
  
  protected
  
  def can_handle?(request)
    raise NotImplementedError
  end
  
  def process(request)
    raise NotImplementedError
  end
end

class ContentTypeHandler < Handler
  def can_handle?(request)
    request.content_type_missing?
  end
  
  def process(request)
    request.set_content_type('application/json')
    @next_handler&.handle(request)
  end
end

class AuthenticationHandler < Handler
  def initialize(auth_service)
    @auth_service = auth_service
  end
  
  def can_handle?(request)
    request.requires_authentication?
  end
  
  def process(request)
    unless @auth_service.valid?(request.credentials)
      raise AuthenticationError, 'Invalid credentials'
    end
    @next_handler&.handle(request)
  end
end

class LoggingHandler < Handler
  def initialize(logger)
    @logger = logger
  end
  
  def can_handle?(request)
    true # Logging applies to all requests
  end
  
  def process(request)
    @logger.info("Processing request: #{request.id}")
    result = @next_handler&.handle(request)
    @logger.info("Completed request: #{request.id}")
    result
  end
end

Proc-Based Chains: Ruby's first-class functions enable chain implementation using procs, reducing boilerplate for simple handlers.

class ProcChain
  def initialize
    @chain = []
  end
  
  def add(handler_proc)
    @chain << handler_proc
    self
  end
  
  def call(request)
    execute_chain(request, 0)
  end
  
  private
  
  def execute_chain(request, index)
    return nil if index >= @chain.size
    
    handler = @chain[index]
    next_handler = ->(req) { execute_chain(req, index + 1) }
    
    handler.call(request, next_handler)
  end
end

# Usage
chain = ProcChain.new
  .add(->(req, next_handler) {
    return false if req[:value] < 0
    next_handler.call(req)
  })
  .add(->(req, next_handler) {
    req[:doubled] = req[:value] * 2
    next_handler.call(req)
  })
  .add(->(req, next_handler) {
    puts "Final value: #{req[:doubled]}"
    true
  })

chain.call(value: 5)

Module-Based Composition: Ruby modules provide an alternative approach using mixin composition and method aliasing.

module ChainLink
  attr_accessor :next_handler
  
  def call(request)
    result = handle_request(request)
    return result if result == :stop
    
    @next_handler ? @next_handler.call(request) : request
  end
  
  def handle_request(request)
    :continue
  end
end

class ValidationLink
  include ChainLink
  
  def handle_request(request)
    if request[:data].nil?
      request[:errors] = ['Data required']
      return :stop
    end
    :continue
  end
end

class TransformationLink
  include ChainLink
  
  def handle_request(request)
    request[:data] = request[:data].upcase
    :continue
  end
end

class PersistenceLink
  include ChainLink
  
  def initialize(repository)
    @repository = repository
  end
  
  def handle_request(request)
    @repository.save(request[:data])
    request[:saved] = true
    :stop
  end
end

Rack-Style Middleware: Ruby's Rack specification exemplifies Chain of Responsibility in web contexts.

class Middleware
  def initialize(app)
    @app = app
  end
  
  def call(env)
    @app.call(env)
  end
end

class TimingMiddleware < Middleware
  def call(env)
    start_time = Time.now
    status, headers, body = @app.call(env)
    duration = Time.now - start_time
    headers['X-Response-Time'] = duration.to_s
    [status, headers, body]
  end
end

class AuthMiddleware < Middleware
  def initialize(app, auth_service)
    super(app)
    @auth_service = auth_service
  end
  
  def call(env)
    token = env['HTTP_AUTHORIZATION']
    unless @auth_service.valid_token?(token)
      return [401, {'Content-Type' => 'text/plain'}, ['Unauthorized']]
    end
    @app.call(env)
  end
end

class CachingMiddleware < Middleware
  def initialize(app, cache)
    super(app)
    @cache = cache
  end
  
  def call(env)
    cache_key = env['PATH_INFO']
    cached = @cache.get(cache_key)
    return cached if cached
    
    response = @app.call(env)
    @cache.set(cache_key, response)
    response
  end
end

# Building the middleware stack
app = ->(env) { [200, {'Content-Type' => 'text/plain'}, ['Hello World']] }
stack = TimingMiddleware.new(
  AuthMiddleware.new(
    CachingMiddleware.new(app, cache),
    auth_service
  )
)

Builder Pattern Integration: Combining Chain of Responsibility with Builder pattern simplifies chain construction.

class HandlerChainBuilder
  def initialize
    @handlers = []
  end
  
  def add_validation(validator_class, *args)
    @handlers << validator_class.new(*args)
    self
  end
  
  def add_transformation(transformer_class, *args)
    @handlers << transformer_class.new(*args)
    self
  end
  
  def add_custom(handler)
    @handlers << handler
    self
  end
  
  def build
    return nil if @handlers.empty?
    
    @handlers.each_cons(2) do |current, next_handler|
      current.next_handler = next_handler
    end
    
    @handlers.first
  end
end

# Usage
chain = HandlerChainBuilder.new
  .add_validation(LengthValidator, 5)
  .add_validation(FormatValidator, /^\w+$/)
  .add_transformation(UppercaseTransformer)
  .add_custom(CustomHandler.new)
  .build

Practical Examples

Expense Approval System: Organizations often route expense requests through multiple approval levels based on amount and type.

class ExpenseRequest
  attr_accessor :amount, :type, :description, :approved_by
  
  def initialize(amount, type, description)
    @amount = amount
    @type = type
    @description = description
    @approved_by = []
  end
end

class ApprovalHandler
  attr_accessor :next_handler
  
  def approve(request)
    if can_approve?(request)
      process_approval(request)
    elsif @next_handler
      @next_handler.approve(request)
    else
      reject_request(request)
    end
  end
  
  protected
  
  def can_approve?(request)
    raise NotImplementedError
  end
  
  def process_approval(request)
    raise NotImplementedError
  end
  
  def reject_request(request)
    raise ApprovalError, "No authority to approve #{request.type} expense of #{request.amount}"
  end
end

class TeamLeadApprover < ApprovalHandler
  def can_approve?(request)
    request.amount <= 500
  end
  
  def process_approval(request)
    request.approved_by << 'Team Lead'
    puts "Team Lead approved #{request.amount} for #{request.description}"
    true
  end
end

class ManagerApprover < ApprovalHandler
  def can_approve?(request)
    request.amount <= 5000
  end
  
  def process_approval(request)
    request.approved_by << 'Manager'
    puts "Manager approved #{request.amount} for #{request.description}"
    true
  end
end

class DirectorApprover < ApprovalHandler
  def can_approve?(request)
    request.amount <= 50000
  end
  
  def process_approval(request)
    request.approved_by << 'Director'
    puts "Director approved #{request.amount} for #{request.description}"
    true
  end
end

class CFOApprover < ApprovalHandler
  def can_approve?(request)
    true # CFO can approve any amount
  end
  
  def process_approval(request)
    request.approved_by << 'CFO'
    puts "CFO approved #{request.amount} for #{request.description}"
    true
  end
end

# Setup approval chain
team_lead = TeamLeadApprover.new
manager = ManagerApprover.new
director = DirectorApprover.new
cfo = CFOApprover.new

team_lead.next_handler = manager
manager.next_handler = director
director.next_handler = cfo

# Process various requests
small_expense = ExpenseRequest.new(300, 'office_supplies', 'Printer paper')
team_lead.approve(small_expense)
# => Team Lead approved 300 for Printer paper

large_expense = ExpenseRequest.new(25000, 'equipment', 'Server upgrade')
team_lead.approve(large_expense)
# => Director approved 25000 for Server upgrade

Logging System with Multiple Destinations: Applications often route log messages to different destinations based on severity level.

class LogMessage
  attr_reader :level, :message, :timestamp, :context
  
  LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }
  
  def initialize(level, message, context = {})
    @level = level
    @message = message
    @timestamp = Time.now
    @context = context
  end
  
  def level_value
    LEVELS[@level]
  end
end

class LogHandler
  attr_accessor :next_handler
  attr_reader :threshold
  
  def initialize(threshold)
    @threshold = threshold
  end
  
  def log(message)
    write_log(message) if should_log?(message)
    @next_handler&.log(message)
  end
  
  protected
  
  def should_log?(message)
    message.level_value >= LogMessage::LEVELS[@threshold]
  end
  
  def write_log(message)
    raise NotImplementedError
  end
end

class ConsoleLogger < LogHandler
  def write_log(message)
    puts "[#{message.timestamp}] #{message.level.upcase}: #{message.message}"
  end
end

class FileLogger < LogHandler
  def initialize(threshold, filepath)
    super(threshold)
    @filepath = filepath
  end
  
  def write_log(message)
    File.open(@filepath, 'a') do |file|
      file.puts "#{message.timestamp}|#{message.level}|#{message.message}|#{message.context.inspect}"
    end
  end
end

class EmailLogger < LogHandler
  def initialize(threshold, recipients)
    super(threshold)
    @recipients = recipients
  end
  
  def write_log(message)
    # Send email to recipients
    subject = "Application #{message.level.upcase}: #{message.message[0..50]}"
    body = "Time: #{message.timestamp}\nLevel: #{message.level}\nMessage: #{message.message}\nContext: #{message.context.inspect}"
    send_email(@recipients, subject, body)
  end
  
  private
  
  def send_email(recipients, subject, body)
    puts "Sending email to #{recipients.join(', ')}: #{subject}"
  end
end

class MetricsLogger < LogHandler
  def initialize(threshold, metrics_service)
    super(threshold)
    @metrics_service = metrics_service
  end
  
  def write_log(message)
    @metrics_service.increment("log.#{message.level}")
    @metrics_service.record("log.latency", Time.now - message.timestamp)
  end
end

# Setup logging chain
console = ConsoleLogger.new(:debug)
file = FileLogger.new(:info, 'application.log')
email = EmailLogger.new(:error, ['ops@example.com'])
metrics = MetricsLogger.new(:warn, MetricsService.new)

console.next_handler = file
file.next_handler = metrics
metrics.next_handler = email

# Usage
logger = console

logger.log(LogMessage.new(:debug, 'Processing user request'))
# => [2025-01-15 10:30:00] DEBUG: Processing user request

logger.log(LogMessage.new(:error, 'Database connection failed', database: 'users'))
# => Logs to console, file, metrics, and sends email

HTTP Request Processing Pipeline: Web applications process incoming requests through multiple stages including authentication, rate limiting, and caching.

class HTTPRequest
  attr_accessor :path, :method, :headers, :body, :user
  attr_reader :metadata
  
  def initialize(path, method, headers = {}, body = nil)
    @path = path
    @method = method
    @headers = headers
    @body = body
    @metadata = {}
  end
end

class HTTPHandler
  attr_accessor :next_handler
  
  def handle(request)
    raise NotImplementedError
  end
end

class CORSHandler < HTTPHandler
  def handle(request)
    request.headers['Access-Control-Allow-Origin'] = '*'
    request.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
    request.metadata[:cors_applied] = true
    @next_handler&.handle(request)
  end
end

class RateLimitHandler < HTTPHandler
  def initialize(limit, window)
    @limit = limit
    @window = window
    @requests = Hash.new { |h, k| h[k] = [] }
  end
  
  def handle(request)
    client_id = request.headers['X-Client-ID'] || request.headers['REMOTE_ADDR']
    current_time = Time.now
    
    @requests[client_id].reject! { |time| current_time - time > @window }
    
    if @requests[client_id].size >= @limit
      raise RateLimitError, "Rate limit exceeded: #{@limit} requests per #{@window} seconds"
    end
    
    @requests[client_id] << current_time
    request.metadata[:rate_limit_remaining] = @limit - @requests[client_id].size
    
    @next_handler&.handle(request)
  end
end

class AuthenticationHandler < HTTPHandler
  def initialize(auth_service)
    @auth_service = auth_service
  end
  
  def handle(request)
    token = request.headers['Authorization']&.sub(/^Bearer /, '')
    
    if token.nil? || token.empty?
      raise AuthenticationError, 'Missing authentication token'
    end
    
    user = @auth_service.validate_token(token)
    if user.nil?
      raise AuthenticationError, 'Invalid authentication token'
    end
    
    request.user = user
    request.metadata[:authenticated] = true
    
    @next_handler&.handle(request)
  end
end

class CacheHandler < HTTPHandler
  def initialize(cache)
    @cache = cache
  end
  
  def handle(request)
    return cached_response(request) if cacheable?(request)
    @next_handler&.handle(request)
  end
  
  private
  
  def cacheable?(request)
    request.method == 'GET' && !request.headers['Cache-Control']&.include?('no-cache')
  end
  
  def cached_response(request)
    cache_key = "#{request.method}:#{request.path}"
    cached = @cache.get(cache_key)
    
    if cached
      request.metadata[:cache_hit] = true
      return cached
    end
    
    response = @next_handler&.handle(request)
    @cache.set(cache_key, response, ttl: 300) if response
    request.metadata[:cache_miss] = true
    response
  end
end

class BusinessLogicHandler < HTTPHandler
  def handle(request)
    # Actual business logic processing
    {
      status: 200,
      body: { message: "Processed #{request.path}" },
      metadata: request.metadata
    }
  end
end

# Build request processing pipeline
cors = CORSHandler.new
rate_limit = RateLimitHandler.new(100, 60)
auth = AuthenticationHandler.new(AuthService.new)
cache = CacheHandler.new(Cache.new)
business = BusinessLogicHandler.new

cors.next_handler = rate_limit
rate_limit.next_handler = auth
auth.next_handler = cache
cache.next_handler = business

# Process request
request = HTTPRequest.new('/api/users', 'GET', {
  'Authorization' => 'Bearer valid_token_here',
  'X-Client-ID' => 'client_123'
})

response = cors.handle(request)

Common Patterns

Early Termination Pattern: Handlers that detect terminal conditions stop the chain immediately rather than passing the request forward.

class ValidationChain
  class Handler
    attr_accessor :next_handler
    
    def validate(data)
      errors = check_errors(data)
      return { valid: false, errors: errors } unless errors.empty?
      
      if @next_handler
        @next_handler.validate(data)
      else
        { valid: true, errors: [] }
      end
    end
    
    protected
    
    def check_errors(data)
      []
    end
  end
  
  class RequiredFieldsValidator < Handler
    def initialize(required_fields)
      @required_fields = required_fields
    end
    
    def check_errors(data)
      @required_fields.select { |field| data[field].nil? }
                     .map { |field| "#{field} is required" }
    end
  end
  
  class TypeValidator < Handler
    def initialize(type_mappings)
      @type_mappings = type_mappings
    end
    
    def check_errors(data)
      @type_mappings.map do |field, expected_type|
        value = data[field]
        next if value.nil?
        "#{field} must be #{expected_type}" unless value.is_a?(expected_type)
      end.compact
    end
  end
end

Request Transformation Pattern: Each handler modifies the request before passing it forward, building up the final result through successive transformations.

class TransformationPipeline
  def initialize
    @transformers = []
  end
  
  def add(transformer)
    @transformers << transformer
    self
  end
  
  def transform(data)
    @transformers.reduce(data) do |current_data, transformer|
      transformer.call(current_data)
    end
  end
end

# Usage with data processing
pipeline = TransformationPipeline.new
  .add(->(data) { data.merge(normalized: data[:value].downcase) })
  .add(->(data) { data.merge(trimmed: data[:normalized].strip) })
  .add(->(data) { data.merge(validated: data[:trimmed].length > 0) })
  .add(->(data) { data.merge(final: data[:validated] ? data[:trimmed] : nil) })

result = pipeline.transform(value: '  HELLO  ')
# => { value: '  HELLO  ', normalized: '  hello  ', trimmed: 'hello', validated: true, final: 'hello' }

Priority-Based Handling: Handlers have associated priorities, and the chain routes requests to handlers based on priority order.

class PriorityHandler
  attr_reader :priority
  attr_accessor :next_handler
  
  def initialize(priority)
    @priority = priority
  end
  
  def handle(request)
    result = try_handle(request)
    return result unless result.nil?
    @next_handler&.handle(request)
  end
  
  protected
  
  def try_handle(request)
    nil
  end
end

class PriorityChain
  def initialize
    @handlers = []
  end
  
  def add_handler(handler)
    @handlers << handler
    rebuild_chain
    self
  end
  
  def handle(request)
    @chain_head&.handle(request)
  end
  
  private
  
  def rebuild_chain
    sorted = @handlers.sort_by { |h| -h.priority }
    sorted.each_cons(2) do |current, next_handler|
      current.next_handler = next_handler
    end
    @chain_head = sorted.first
  end
end

Parallel Chain Pattern: Request broadcasts to multiple handlers simultaneously, collecting results from all handlers.

class ParallelChain
  def initialize
    @handlers = []
  end
  
  def add_handler(handler)
    @handlers << handler
    self
  end
  
  def handle(request)
    results = @handlers.map do |handler|
      begin
        handler.handle(request)
      rescue => e
        { error: e.message, handler: handler.class.name }
      end
    end
    
    {
      request: request,
      results: results,
      success_count: results.count { |r| !r.is_a?(Hash) || !r.key?(:error) }
    }
  end
end

class NotificationHandler
  def handle(message)
    # Send notification
    { sent: true, channel: self.class.name }
  end
end

class EmailNotification < NotificationHandler; end
class SMSNotification < NotificationHandler; end
class PushNotification < NotificationHandler; end

# Broadcast to all notification channels
notifications = ParallelChain.new
  .add_handler(EmailNotification.new)
  .add_handler(SMSNotification.new)
  .add_handler(PushNotification.new)

result = notifications.handle(message: 'Important update')

Conditional Chain Pattern: Handler selection based on request properties determines which chain processes the request.

class ConditionalRouter
  def initialize
    @routes = []
  end
  
  def add_route(condition, handler)
    @routes << { condition: condition, handler: handler }
    self
  end
  
  def handle(request)
    route = @routes.find { |r| r[:condition].call(request) }
    
    if route
      route[:handler].handle(request)
    else
      raise RoutingError, "No handler found for request: #{request.inspect}"
    end
  end
end

# Route based on request type
router = ConditionalRouter.new
  .add_route(->(req) { req[:type] == :payment }, PaymentChain.new)
  .add_route(->(req) { req[:type] == :refund }, RefundChain.new)
  .add_route(->(req) { req[:type] == :query }, QueryChain.new)
  .add_route(->(req) { true }, DefaultChain.new) # Catch-all

Reference

Core Components

Component Responsibility Implementation
Handler Interface Defines request handling contract Abstract class or module with handle method
Concrete Handler Implements specific handling logic Subclass implementing can_handle? and process
Next Handler Reference Links to successor in chain attr_accessor or instance variable
Chain Builder Constructs and links handlers Utility class for chain assembly
Client Initiates requests Object that calls first handler

Handler Responsibilities

Responsibility Description Implementation Approach
Request Evaluation Determine if handler can process request Implement can_handle? method
Request Processing Execute handling logic Implement process method
Delegation Decision Choose to handle or forward Conditional logic in handle method
Chain Termination Stop propagation when appropriate Return without calling next handler
Error Handling Manage errors during processing Begin-rescue blocks or error handlers

Chain Configuration Patterns

Pattern Use Case Implementation
Static Chain Fixed handler sequence Link handlers at initialization
Dynamic Chain Runtime handler composition Build chain based on request properties
Conditional Chain Route to different chains Router selects chain based on conditions
Priority Chain Order by handler priority Sort handlers before linking
Parallel Chain Broadcast to multiple handlers Execute all handlers, collect results

Common Handler Types

Handler Type Purpose Example Use Case
Validator Check request validity Input validation, business rule checking
Transformer Modify request data Data normalization, format conversion
Authenticator Verify credentials User authentication, token validation
Authorizer Check permissions Role-based access control
Logger Record processing information Audit logging, debugging
Cache Store and retrieve results Response caching, memoization
Rate Limiter Control request frequency API throttling, DDoS prevention
Error Handler Process failures Error recovery, fallback logic

Request Flow Control

Control Mechanism Effect Implementation
Process and Stop Handle request, prevent forwarding Return result without calling next
Process and Continue Handle request, allow forwarding Call next handler after processing
Skip and Forward Don't handle, pass to next Call next handler immediately
Reject Request Terminate with error Raise exception or return error
Broadcast Send to multiple handlers Iterate all handlers in parallel

Anti-Patterns

Anti-Pattern Problem Solution
Infinite Loop Handler forwards to itself Ensure acyclic chain structure
Missing Termination No handler processes request Add default handler at chain end
Tight Coupling Handlers depend on specific handlers Depend only on handler interface
Overly Long Chains Performance degradation Break into smaller chains or optimize
Side Effect Ordering Handlers make assumptions about order Make handlers order-independent
Handler Mutation Handlers modify each other Keep handlers isolated and immutable

Performance Characteristics

Aspect Characteristic Optimization Strategy
Time Complexity O(n) worst case Place likely handlers early
Space Complexity O(n) for chain Reuse handlers across chains
Handler Overhead Per-handler invocation cost Minimize handler logic
Request Copying Potential object duplication Pass by reference when safe
Chain Construction One-time setup cost Build chains at initialization

Testing Strategies

Strategy Focus Approach
Unit Testing Individual handler logic Test handlers in isolation
Integration Testing Chain behavior Test full chain execution
Mock Successors Handler delegation Mock next handler
Edge Case Testing Boundary conditions Test empty chains, single handlers
Error Scenario Testing Failure handling Test exception propagation
Performance Testing Chain efficiency Benchmark with various chain lengths