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
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.
flowchart LR Client["ValidationChainService"] H1["InventoryCheck"] H2["PaymentValidation"] H3["FraudDetection"] H4["AddressValidation"] Client -->|"Validate(order)"| H1 H1 -->|"next"| H2 H2 -->|"next"| H3 H3 -->|"next"| H4
- Handler interface:
OrderValidatordefinesSetNext,Validate, andCheck. - Base handler: Shared forwarding logic lives in
BaseHandler. - Concrete handlers: Inventory, payment, fraud, and address validators each own one responsibility.
- Client:
ValidationChainServicewires 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
| Pros | Cons |
|---|---|
| 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.