I. Chain of Responsibility — Behavioral Pattern
The Chain of Responsibility pattern lets you pass requests along a chain of handlers. Each handler decides either to process the request or to forward it to the next handler in the chain.
This pattern decouples the sender of a request from its receivers by giving more than one object a chance to handle the request. It avoids coupling the sender to a specific receiver and allows you to build flexible pipelines at runtime.
The pattern is especially powerful in middleware-style architectures — authentication, rate limiting, logging, caching — where each concern can be isolated into its own handler and composed in any order.
II. Real-world Example
Imagine an HTTP client pipeline with three middleware layers:
- AuthHandler — checks that a valid token is present. If the token is empty, it immediately rejects the request.
- RateLimitHandler — uses an atomic counter to enforce a maximum number of concurrent requests. Excess requests are dropped without reaching the network.
- FetchHandler — the terminal handler that actually performs the HTTP call and returns the response.
Each handler knows only about the next one. The client that kicks off the chain has no idea how many handlers exist or what they do.

III. Implementation
handler.go — the Handler interface and BaseHandler embed:
package chain
type Handler interface {
SetNext(handler Handler) Handler
Handle(url string) (interface{}, error)
}
type BaseHandler struct {
next Handler
}
func (h *BaseHandler) SetNext(handler Handler) Handler {
h.next = handler
return handler
}
func (h *BaseHandler) nextHandle(url string) (interface{}, error) {
if h.next == nil {
return nil, nil
}
return h.next.Handle(url)
}auth_handler.go — validates the auth token:
package chain
import "errors"
type AuthHandler struct {
BaseHandler
token string
}
func NewAuthHandler(token string) *AuthHandler {
return &AuthHandler{token: token}
}
func (h *AuthHandler) Handle(url string) (interface{}, error) {
if h.token == "" {
return nil, errors.New("missing auth token")
}
return h.nextHandle(url)
}rate_limit_handler.go — enforces concurrency limits atomically:
package chain
import (
"errors"
"sync/atomic"
)
type RateLimitHandler struct {
BaseHandler
limit int64
current int64
}
func NewRateLimitHandler(limit int64) *RateLimitHandler {
return &RateLimitHandler{limit: limit}
}
func (h *RateLimitHandler) Handle(url string) (interface{}, error) {
if atomic.AddInt64(&h.current, 1) > h.limit {
atomic.AddInt64(&h.current, -1)
return nil, errors.New("rate limit exceeded")
}
result, err := h.nextHandle(url)
if err != nil {
atomic.AddInt64(&h.current, -1)
}
return result, err
}fetch_handler.go — the terminal handler that returns a response:
package chain
import "fmt"
type FetchHandler struct {
BaseHandler
}
func NewFetchHandler() *FetchHandler {
return &FetchHandler{}
}
func (h *FetchHandler) Handle(url string) (interface{}, error) {
return fmt.Sprintf("response from %s", url), nil
}main.go — wiring the chain:
auth := chain.NewAuthHandler("secret-token")
rateLimit := chain.NewRateLimitHandler(2)
fetch := chain.NewFetchHandler()
auth.SetNext(rateLimit).SetNext(fetch)
response, err := auth.Handle("https://example.com")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Response:", response)
}
_, err = chain.NewAuthHandler("").Handle("https://example.com")
if err != nil {
fmt.Println("Auth error:", err)
}IV. Explain the example above
Step 1 — Build the chain
SetNext returns the handler it received, so you can chain calls fluently: auth → rateLimit → fetch. At this point no request has been sent yet.
Step 2 — Happy path request
auth.Handle("https://example.com")
→ token "secret-token" ✓ → calls rateLimit.Handle
→ current=1 ≤ limit=2 ✓ → calls fetch.Handle
→ returns "response from https://example.com"Output:
Response: response from https://example.comStep 3 — Missing token
chain.NewAuthHandler("").Handle("https://example.com")
→ token is empty → returns error immediatelyOutput:
Auth error: missing auth tokenStep 4 — Rate limit exceeded
If three concurrent requests hit RateLimitHandler with limit=2, the third call atomically increments current to 3, sees it exceeds the limit, decrements back to 2, and returns an error — without ever reaching FetchHandler.
Rate limit error: rate limit exceededNotice how FetchHandler is completely unaware of auth or rate limiting. You can add, remove, or reorder handlers without touching the others.
V. Conclusion
When to use it:
- You need to process a request through multiple independent steps (middleware pipelines, validation chains, event processing).
- The set of handlers or their order may change at runtime.
- You want each handler to have a single responsibility.
When to avoid it:
- Every request must be handled — if no handler in the chain processes it, you get a silent
nil, nilreturn. You need to design a fallback or terminal handler.
- Deep chains make debugging harder because the execution path is dynamic and not obvious from the call site.
- For simple linear transformations, a plain function slice (e.g.,
[]Middleware) is often clearer and cheaper.
No pattern is universally superior. Chain of Responsibility shines when you need composable, open-ended pipelines — but keep the chain short and ensure every path has a defined outcome.
VI. References
- Go Design Patterns (Mario Castro Contreras)




