Behavioral Complexity: Medium

Chain of Responsibility in Go

Build validation and request-processing pipelines with small handlers composed through interfaces.

The Problem

It is common to start with one large validation function that checks inventory, payment rules, fraud signals, and shipping data all at once. That grows into brittle code quickly: every new rule touches the same function, testing becomes tedious, and reordering behavior is risky.

func ValidateOrder(order Order) error {
    // inventory, payment, fraud, and address logic all mixed together
    return nil
}

That is especially awkward in Go, where small functions and explicit composition are usually easier to maintain.

The Solution

Chain of Responsibility breaks the workflow into focused handlers. Each handler checks one concern and optionally forwards the request to the next handler. In Go, the chain is built with a small interface and struct composition, not inheritance.

Structure

  • Handler interface: OrderValidator defines SetNext, Validate, and Check.
  • Base handler: Shared forwarding logic lives in BaseHandler.
  • Concrete handlers: Inventory, payment, fraud, and address validators each own one responsibility.
  • Client: ValidationChainService wires the chain once and executes it.

Implementation

This example runs an order through four validation steps and collects the result from each handler so the caller can inspect the full pipeline.

package main

type ValidationResult struct {
	HandlerName string
	Valid       bool
	Errors      []string
}

type Item struct {
	Name  string
	Price float64
	Qty   int
}

type Address struct {
	Street  string
	City    string
	Zip     string
	Country string
}

type OrderData struct {
	Items           []Item
	PaymentMethod   string
	TotalAmount     float64
	ShippingAddress Address
}

type OrderValidator interface {
	SetNext(OrderValidator) OrderValidator
	Validate(order OrderData) []ValidationResult
	Check(order OrderData) ValidationResult
}

Best Practices

  • Use small handlers with one reason to change.
  • Prefer composition. The shared chain behavior belongs in an embedded helper, not a deep type hierarchy.
  • Keep handler contracts explicit. Returning structured validation results is usually easier to debug than a single boolean.
  • Wire the chain at the edge of the system so the caller does not know which concrete validators exist.
  • Do not build a chain when a plain slice of functions would do. The pattern is useful when sequencing and substitution matter.

When to Use

  • A request must pass through several optional or reorderable steps.
  • Different deployments or workflows need different validation chains.
  • You want each step to be testable in isolation.

When NOT to Use

  • There is only one step, so a direct call is clearer.
  • The full pipeline is fixed and trivial, making a slice of plain functions simpler.
  • The chain becomes so long that tracing control flow is harder than a direct implementation.