🎧 Analogy
You call customer support. Tier-1 tries to help; if it’s beyond them, they escalate to tier-2; if still stuck, to a manager. You made one call — each level either resolves your issue or passes it up. You never had to know who’d ultimately handle it.
The problem
A request could be handled by one of several handlers, but the sender shouldn’t be wired to a specific one. Chain of Responsibility links handlers together; each gets a shot and either handles the request or passes it on. Adding, removing, or reordering handlers doesn’t touch the sender.
Structure
graph LR
R["Request: $5000"] --> L["Team Lead<br/>≤ $100?"]
L -->|too big| M["Manager<br/>≤ $1000?"]
M -->|too big| D["Director<br/>≤ $10000?"]
D -->|handles it| OK["approved"]
Idiomatic Go
Each approver handles amounts up to its limit, or forwards to the next. Edit and Run:
package main
import "fmt"
type Request struct{ amount int }
// Approver handles a request or passes it along.
type Approver interface {
Approve(r Request)
SetNext(Approver)
}
// base provides the "pass it on" behavior via embedding.
type base struct{ next Approver }
func (b *base) SetNext(a Approver) { b.next = a }
func (b *base) passOn(r Request) {
if b.next != nil {
b.next.Approve(r)
} else {
fmt.Printf("$%d needs board approval\n", r.amount)
}
}
type TeamLead struct{ base }
func (t *TeamLead) Approve(r Request) {
if r.amount <= 100 {
fmt.Printf("Team lead approved $%d\n", r.amount)
return
}
t.passOn(r)
}
type Manager struct{ base }
func (m *Manager) Approve(r Request) {
if r.amount <= 1000 {
fmt.Printf("Manager approved $%d\n", r.amount)
return
}
m.passOn(r)
}
type Director struct{ base }
func (d *Director) Approve(r Request) {
if r.amount <= 10000 {
fmt.Printf("Director approved $%d\n", r.amount)
return
}
d.passOn(r)
}
func main() {
lead, mgr, dir := &TeamLead{}, &Manager{}, &Director{}
lead.SetNext(mgr)
mgr.SetNext(dir)
for _, amt := range []int{50, 500, 5000, 50000} {
lead.Approve(Request{amount: amt})
}
}
🐹 You already use this: HTTP middleware
The most idiomatic Go chain is func(next http.Handler) http.Handler. Each middleware can do work and call next.ServeHTTP — or short-circuit (reject an unauthenticated request and never call next). Composing logging, auth, gzip, and recovery into one handler is Chain of Responsibility, and it’s the backbone of every Go web framework.
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path)
next.ServeHTTP(w, r) // pass along the chain
})
}
Pitfalls
⚠️ Mind the end of the chain
If no handler accepts the request, it can silently fall off the end. Decide explicitly what that means — a default handler, an error, or a panic — as the example does with “needs board approval.” A request that vanishes unhandled is a bug that’s hard to spot.
When to use it — and when not
✅ Reach for it when
- More than one object might handle a request, and the handler isn't known in advance.
- You want to decouple the sender from the concrete receiver.
- The set or order of handlers should be configurable — middleware, validation pipelines, approval flows.
⛔ Think twice when
- Exactly one object always handles the request — call it directly.
- A request might silently fall off the end of the chain unhandled and that's a bug.
Related patterns
Turn a request into a standalone object, letting you parameterize, queue, log, and undo operations.
STRUCTURAL DecoratorAttach new behavior to an object by wrapping it in another object of the same interface — without changing the original's type.
BEHAVIORAL MediatorCentralize communication between objects in a mediator, so they don't refer to each other directly.
BEHAVIORAL ObserverDefine a one-to-many dependency so that when one object changes state, all its dependents are notified automatically.
Check your understanding
Score: 0 / 31. What does a handler in the chain do?
Each handler decides: if it can handle the request it does (often stopping the chain); otherwise it forwards to the next one.
2. Which Go idiom IS Chain of Responsibility?
HTTP middleware composes handlers where each can act and then call next.ServeHTTP — or short-circuit — which is exactly a chain of responsibility.
3. What does the pattern decouple?
The sender just hands the request to the front of the chain; it never needs to know which handler (if any) will process it.