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

Chain of Responsibility Pattern
Step 1 of 6

The Client

ValidationChainService wires the chain once and calls Validate. It knows only the first handler — not how many steps follow or which validators are in the chain.

  • 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
}

Real-World Analogy

Think of airport security. Your bag moves through identity check, scanning, secondary inspection, and final clearance. Each checkpoint focuses on one responsibility and passes the traveler forward only if that step succeeds.

Pros and Cons

ProsCons
Breaks large workflows into small, reorderable steps.Control flow can become harder to trace across many handlers.
Makes each handler easier to test in isolation.Long chains may hide where failures really come from.
Lets different deployments assemble different pipelines.Can be heavier than a plain slice of functions for simple fixed pipelines.

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.