Chain of Responsibility Pattern #

An HTTP request arrives at a server. Before reaching the business handler, it must pass several checks: is there a valid JWT token? Has this user verified their email? Has the rate limit been exceeded? Does the request body exceed the size limit? Without Chain of Responsibility, all these checks end up in one large function that is hard to test and hard to reorder. With Chain of Responsibility, every check is a self-contained handler that can stand alone, and those handlers are chained in sequence — if one handler fails, it breaks the chain and returns an error; if it passes, the request moves to the next handler. Adding a new check means adding one handler and inserting it in the right position in the chain — without changing any other handler at all.

What Is Chain of Responsibility? #

Chain of Responsibility (CoR) is a behavioral design pattern that flows a request through a sequence of handlers, where each handler decides: handle this request, pass it to the next handler, or stop the chain with an error.

There are two different operation modes in Chain of Responsibility:

  • Stop-first — a handler stops the chain as soon as one handles or fails; suited to validation and authentication
  • Pipeline — every handler processes the request and passes it on; all handlers always execute; suited to data transformation and enrichment

This difference matters because it determines how the chain behaves when a handler “handles” a request.

flowchart LR
    subgraph "Stop-first (Validation)"
        R1[Request] --> H1A[Auth\\nHandler]
        H1A -->|"passes"| H2A[Rate\\nLimit]
        H2A -->|"passes"| H3A[Business\\nHandler]
        H1A -->|"fails → stop"| E1[Error 401]
        H2A -->|"fails → stop"| E2[Error 429]
    end

    subgraph "Pipeline (Transformation)"
        R2[Request] --> H1B[Log\\nHandler]
        H1B -->|"log + pass on"| H2B[Enrich\\nHandler]
        H2B -->|"enrich + pass on"| H3B[Business\\nHandler]
    end

The Problem It Solves #

Chain of Responsibility solves a very specific problem: how to organize a series of sequential handling steps without hardcoding the order and logic in one place.

The Problem: All Validation Piled in One Place #

// ANTI-PATTERN: all validation in one function — hard to test and hard to reorder
func HandleRequest(w http.ResponseWriter, r *http.Request) {
    // Validation 1: authentication
    token := r.Header.Get("Authorization")
    if token == "" {
        http.Error(w, "unauthorized", 401)
        return
    }
    claims, err := validateJWT(token)
    if err != nil {
        http.Error(w, "invalid token", 401)
        return
    }

    // Validation 2: verified email
    if !claims.EmailVerified {
        http.Error(w, "email not verified", 403)
        return
    }

    // Validation 3: rate limit
    if exceeded := rateLimiter.Check(claims.UserID); exceeded {
        http.Error(w, "rate limit exceeded", 429)
        return
    }

    // Validation 4: request body size
    if r.ContentLength > 1<<20 { // 1MB
        http.Error(w, "request too large", 413)
        return
    }

    // Only now the business logic
    actualHandler(w, r)
    // Adding new validation means coming here and typing new code
}

// CORRECT: every validation is a self-contained handler that can be chained
chain := BuildChain(
    NewAuthHandler(jwtSvc),
    NewEmailVerifiedHandler(),
    NewRateLimitHandler(rateLimiter),
    NewBodySizeHandler(1<<20),
)
chain.Handle(w, r)
// Adding a new validation = create a new handler, insert it into the chain

Two Handler Structures in Go #

Go has two idiomatic ways to implement Chain of Responsibility, each suited to a different context.

flowchart TD
    subgraph "Struct-based (Linked List)"
        direction LR
        H1[Handler A\\nnext → ] --> H2[Handler B\\nnext → ] --> H3[Handler C\\nnext: nil]
    end

    subgraph "Functional (Middleware Stack)"
        direction LR
        F1["func(next Handler) Handler\\ncalls next inside"]
        F2["Chained with wrap:\\nC(B(A(base)))"]
    end
ApproachHow It WorksBest For
Struct-basedHandler stores a reference to next; calls next.Handle() explicitlyRequests that can stop mid-chain (validation)
Functional middlewareA function receives next and returns a new handler; always calls next unless there is an errorHTTP middleware, gRPC interceptors

Full Implementation: HTTP Middleware Chain #

Struct-based Handler Interface #

package middleware

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

// Request carries all the data flowing through the handler chain.
// The Context field lets handlers write data for the next handler.
type Request struct {
    HTTPRequest *http.Request
    Context     context.Context
    UserID      string
    Claims      map[string]interface{}
    StartTime   time.Time
}

// Response carries the result built by the handlers.
type Response struct {
    StatusCode int
    Body       interface{}
    Headers    map[string]string
    Error      error
}

// Handler is the interface for every handler in the chain.
type Handler interface {
    Handle(req *Request, res *Response)
    SetNext(handler Handler)
    Name() string
}

// BaseHandler provides the default implementation for SetNext and Handle.
// Embedded by all concrete handlers to avoid duplication.
type BaseHandler struct {
    next Handler
}

func (b *BaseHandler) SetNext(handler Handler) {
    b.next = handler
}

// PassToNext forwards the request to the next handler in the chain.
// Called by a concrete handler after it successfully processes the request.
func (b *BaseHandler) PassToNext(req *Request, res *Response) {
    if b.next != nil {
        b.next.Handle(req, res)
    }
}

Concrete Handlers #

package middleware

import (
    "fmt"
    "net/http"
    "strings"
    "sync"
    "time"
)

// AuthHandler validates the JWT token in the request.
type AuthHandler struct {
    BaseHandler
    jwtSecret string
}

func NewAuthHandler(secret string) Handler {
    return &AuthHandler{jwtSecret: secret}
}

func (h *AuthHandler) Handle(req *Request, res *Response) {
    authHeader := req.HTTPRequest.Header.Get("Authorization")
    if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
        res.StatusCode = http.StatusUnauthorized
        res.Error = fmt.Errorf("missing or malformed Authorization header")
        return // stop the chain — do not call PassToNext
    }

    token := strings.TrimPrefix(authHeader, "Bearer ")
    claims, err := validateJWT(token, h.jwtSecret)
    if err != nil {
        res.StatusCode = http.StatusUnauthorized
        res.Error = fmt.Errorf("invalid token: %w", err)
        return
    }

    // Write the result to the request context for the next handler
    req.UserID = claims["sub"].(string)
    req.Claims = claims

    h.PassToNext(req, res) // passes — forward to the next handler
}

func (h *AuthHandler) Name() string { return "AuthHandler" }


// EmailVerifiedHandler ensures the user has verified their email.
type EmailVerifiedHandler struct {
    BaseHandler
}

func NewEmailVerifiedHandler() Handler {
    return &EmailVerifiedHandler{}
}

func (h *EmailVerifiedHandler) Handle(req *Request, res *Response) {
    emailVerified, ok := req.Claims["email_verified"].(bool)
    if !ok || !emailVerified {
        res.StatusCode = http.StatusForbidden
        res.Error = fmt.Errorf("email not verified: please verify your email before continuing")
        return
    }
    h.PassToNext(req, res)
}

func (h *EmailVerifiedHandler) Name() string { return "EmailVerifiedHandler" }


// RateLimitHandler limits the number of requests per user per minute.
type RateLimitHandler struct {
    BaseHandler
    mu         sync.Mutex
    counters   map[string]*rateLimitEntry
    maxPerMin  int
}

type rateLimitEntry struct {
    count    int
    resetAt  time.Time
}

func NewRateLimitHandler(maxPerMinute int) Handler {
    return &RateLimitHandler{
        counters:  make(map[string]*rateLimitEntry),
        maxPerMin: maxPerMinute,
    }
}

func (h *RateLimitHandler) Handle(req *Request, res *Response) {
    h.mu.Lock()
    entry, ok := h.counters[req.UserID]
    if !ok || time.Now().After(entry.resetAt) {
        entry = &rateLimitEntry{count: 0, resetAt: time.Now().Add(time.Minute)}
        h.counters[req.UserID] = entry
    }
    entry.count++
    count := entry.count
    h.mu.Unlock()

    if count > h.maxPerMin {
        res.StatusCode = http.StatusTooManyRequests
        res.Error = fmt.Errorf("rate limit exceeded: max %d requests per minute", h.maxPerMin)
        res.Headers = map[string]string{
            "Retry-After": "60",
            "X-RateLimit-Limit": fmt.Sprintf("%d", h.maxPerMin),
        }
        return
    }

    h.PassToNext(req, res)
}

func (h *RateLimitHandler) Name() string { return "RateLimitHandler" }


// BodySizeHandler rejects requests with oversized bodies.
type BodySizeHandler struct {
    BaseHandler
    maxBytes int64
}

func NewBodySizeHandler(maxBytes int64) Handler {
    return &BodySizeHandler{maxBytes: maxBytes}
}

func (h *BodySizeHandler) Handle(req *Request, res *Response) {
    if req.HTTPRequest.ContentLength > h.maxBytes {
        res.StatusCode = http.StatusRequestEntityTooLarge
        res.Error = fmt.Errorf("request body too large: max %d bytes", h.maxBytes)
        return
    }
    h.PassToNext(req, res)
}

func (h *BodySizeHandler) Name() string { return "BodySizeHandler" }


// LoggingHandler records every request — it always passes to the next handler.
type LoggingHandler struct {
    BaseHandler
    logger Logger
}

func NewLoggingHandler(logger Logger) Handler {
    return &LoggingHandler{logger: logger}
}

func (h *LoggingHandler) Handle(req *Request, res *Response) {
    start := time.Now()
    h.PassToNext(req, res) // pass first, log after finishing
    duration := time.Since(start)

    h.logger.Info("request processed",
        "user_id", req.UserID,
        "method", req.HTTPRequest.Method,
        "path", req.HTTPRequest.URL.Path,
        "status", res.StatusCode,
        "duration_ms", duration.Milliseconds(),
        "error", res.Error,
    )
}

func (h *LoggingHandler) Name() string { return "LoggingHandler" }

Chain Builder #

package middleware

// ChainBuilder makes assembling a handler chain easy.
type ChainBuilder struct {
    handlers []Handler
}

func NewChainBuilder() *ChainBuilder {
    return &ChainBuilder{}
}

func (b *ChainBuilder) Add(handlers ...Handler) *ChainBuilder {
    b.handlers = append(b.handlers, handlers...)
    return b
}

// Build chains all handlers into a linked list and returns the first handler.
func (b *ChainBuilder) Build() Handler {
    if len(b.handlers) == 0 {
        return nil
    }
    // Chain from back to front
    for i := len(b.handlers) - 2; i >= 0; i-- {
        b.handlers[i].SetNext(b.handlers[i+1])
    }
    return b.handlers[0]
}

// Usage
func buildAPIChain(jwtSecret string, rateLimiter *RateLimitHandler) Handler {
    return NewChainBuilder().
        Add(NewLoggingHandler(logger)).         // 1. log all requests
        Add(NewAuthHandler(jwtSecret)).          // 2. validate the token
        Add(NewEmailVerifiedHandler()).           // 3. check verified email
        Add(NewRateLimitHandler(100)).           // 4. check the rate limit
        Add(NewBodySizeHandler(1 << 20)).        // 5. check the body size
        Build()
}

Functional Middleware: The Idiomatic HTTP Style #

In Go, HTTP middleware is the most natural CoR implementation — every middleware is a function that takes an http.Handler and returns an http.Handler.

// Middleware is the function type for HTTP middleware
type Middleware func(http.Handler) http.Handler

// Chain composes several middleware into one handler.
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
    // Apply in reverse order so execution runs left to right
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

// LoggingMiddleware records every request and its duration.
func LoggingMiddleware(logger *slog.Logger) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            // Wrap the ResponseWriter to capture the status code
            wrapped := &statusRecorder{ResponseWriter: w, statusCode: 200}

            next.ServeHTTP(wrapped, r) // pass to the next handler

            logger.Info("request",
                "method", r.Method,
                "path", r.URL.Path,
                "status", wrapped.statusCode,
                "duration_ms", time.Since(start).Milliseconds(),
            )
        })
    }
}

// statusRecorder captures the status code written to the ResponseWriter
type statusRecorder struct {
    http.ResponseWriter
    statusCode int
}

func (r *statusRecorder) WriteHeader(code int) {
    r.statusCode = code
    r.ResponseWriter.WriteHeader(code)
}

// AuthMiddleware validates a JWT and injects user info into the context.
func AuthMiddleware(secret string) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
            if token == "" {
                http.Error(w, "unauthorized", http.StatusUnauthorized)
                return // stop the chain — do not call next
            }

            claims, err := validateJWT(token, secret)
            if err != nil {
                http.Error(w, "invalid token", http.StatusUnauthorized)
                return
            }

            // Inject the claims into the context for the next handler
            ctx := context.WithValue(r.Context(), contextKeyUserID, claims["sub"])
            next.ServeHTTP(w, r.WithContext(ctx)) // pass on with the new context
        })
    }
}

// RateLimitMiddleware limits requests per IP.
func RateLimitMiddleware(maxPerMin int) Middleware {
    limiter := newIPRateLimiter(maxPerMin)
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := r.RemoteAddr
            if !limiter.Allow(ip) {
                w.Header().Set("Retry-After", "60")
                http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// RecoveryMiddleware catches panics and turns them into 500 errors.
func RecoveryMiddleware(logger *slog.Logger) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if rec := recover(); rec != nil {
                    logger.Error("panic recovered",
                        "panic", fmt.Sprintf("%v", rec),
                        "path", r.URL.Path,
                    )
                    http.Error(w, "internal server error", http.StatusInternalServerError)
                }
            }()
            next.ServeHTTP(w, r)
        })
    }
}

// CORSMiddleware adds CORS headers.
func CORSMiddleware(allowedOrigins []string) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            origin := r.Header.Get("Origin")
            for _, allowed := range allowedOrigins {
                if allowed == "*" || allowed == origin {
                    w.Header().Set("Access-Control-Allow-Origin", origin)
                    w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
                    w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
                    break
                }
            }
            if r.Method == http.MethodOptions {
                w.WriteHeader(http.StatusNoContent)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// Assembly — order from outside to inside
func setupRouter() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/orders", handleOrders)
    mux.HandleFunc("/api/users", handleUsers)

    return Chain(mux,
        RecoveryMiddleware(logger),         // outermost: recovery first
        LoggingMiddleware(logger),           // then log
        CORSMiddleware([]string{"*"}),       // then CORS
        RateLimitMiddleware(100),            // then rate limit
        AuthMiddleware(cfg.JWTSecret),       // then auth
    )
}

Pipeline: All Handlers Always Execute #

The pipeline mode differs from stop-first — every handler processes and passes on the request, building the result incrementally. Useful for data transformation.

// DataPipeline processes data through a series of sequential transformers.
type DataTransformer interface {
    Transform(data map[string]interface{}) (map[string]interface{}, error)
    Name() string
}

// Pipeline runs all transformers in sequence.
// Unlike stop-first CoR — all transformers always execute.
type Pipeline struct {
    transformers []DataTransformer
}

func NewPipeline(transformers ...DataTransformer) *Pipeline {
    return &Pipeline{transformers: transformers}
}

func (p *Pipeline) Process(data map[string]interface{}) (map[string]interface{}, error) {
    current := data
    for _, t := range p.transformers {
        result, err := t.Transform(current)
        if err != nil {
            return nil, fmt.Errorf("transformer %s failed: %w", t.Name(), err)
        }
        current = result
    }
    return current, nil
}

// Example concrete transformers
type SanitizeTransformer struct{}

func (t *SanitizeTransformer) Transform(data map[string]interface{}) (map[string]interface{}, error) {
    result := make(map[string]interface{})
    for k, v := range data {
        if str, ok := v.(string); ok {
            result[k] = strings.TrimSpace(str)
        } else {
            result[k] = v
        }
    }
    return result, nil
}
func (t *SanitizeTransformer) Name() string { return "Sanitize" }

type NormalizeEmailTransformer struct{}

func (t *NormalizeEmailTransformer) Transform(data map[string]interface{}) (map[string]interface{}, error) {
    result := make(map[string]interface{})
    for k, v := range data {
        result[k] = v
    }
    if email, ok := result["email"].(string); ok {
        result["email"] = strings.ToLower(email)
    }
    return result, nil
}
func (t *NormalizeEmailTransformer) Name() string { return "NormalizeEmail" }

type EnrichWithTimestampTransformer struct{}

func (t *EnrichWithTimestampTransformer) Transform(data map[string]interface{}) (map[string]interface{}, error) {
    result := make(map[string]interface{})
    for k, v := range data {
        result[k] = v
    }
    result["processed_at"] = time.Now().UTC().Format(time.RFC3339)
    return result, nil
}
func (t *EnrichWithTimestampTransformer) Name() string { return "EnrichTimestamp" }

// Pipeline usage
func processUserRegistration(input map[string]interface{}) (map[string]interface{}, error) {
    pipeline := NewPipeline(
        &SanitizeTransformer{},
        &NormalizeEmailTransformer{},
        &EnrichWithTimestampTransformer{},
    )
    return pipeline.Process(input)
}

Case Study: Approval Workflow #

Chain of Responsibility is also very well suited to tiered approval systems — requests are processed by different approvers based on value or request type.

// ApprovalRequest represents a request that needs approval.
type ApprovalRequest struct {
    ID          string
    Type        string
    Amount      float64
    RequestedBy string
    Description string
}

// ApprovalResult stores the outcome of the approval process.
type ApprovalResult struct {
    Approved   bool
    ApprovedBy string
    Reason     string
}

// Approver is a handler for the approval workflow.
type Approver interface {
    Approve(req ApprovalRequest) (*ApprovalResult, error)
    SetNext(approver Approver)
    Title() string
}

// BaseApprover provides the SetNext implementation.
type BaseApprover struct {
    next Approver
}

func (a *BaseApprover) SetNext(approver Approver) { a.next = approver }

func (a *BaseApprover) passToNext(req ApprovalRequest) (*ApprovalResult, error) {
    if a.next != nil {
        return a.next.Approve(req)
    }
    return &ApprovalResult{
        Approved: false,
        Reason:   "no approver available for this request",
    }, nil
}

// TeamLeadApprover approves requests up to 10 million.
type TeamLeadApprover struct {
    BaseApprover
    name      string
    threshold float64
}

func NewTeamLeadApprover(name string) *TeamLeadApprover {
    return &TeamLeadApprover{name: name, threshold: 10_000_000}
}

func (a *TeamLeadApprover) Approve(req ApprovalRequest) (*ApprovalResult, error) {
    if req.Amount <= a.threshold {
        fmt.Printf("[%s] Approving request %s (Rp %.0f)\n", a.Title(), req.ID, req.Amount)
        return &ApprovalResult{
            Approved:   true,
            ApprovedBy: a.name,
            Reason:     fmt.Sprintf("amount within team lead threshold (Rp %.0f)", a.threshold),
        }, nil
    }
    fmt.Printf("[%s] Forwarding to the next level (Rp %.0f exceeds threshold)\n",
        a.Title(), req.Amount)
    return a.passToNext(req)
}

func (a *TeamLeadApprover) Title() string { return "Team Lead" }


// ManagerApprover approves requests up to 100 million.
type ManagerApprover struct {
    BaseApprover
    name      string
    threshold float64
}

func NewManagerApprover(name string) *ManagerApprover {
    return &ManagerApprover{name: name, threshold: 100_000_000}
}

func (a *ManagerApprover) Approve(req ApprovalRequest) (*ApprovalResult, error) {
    if req.Amount <= a.threshold {
        fmt.Printf("[%s] Approving request %s (Rp %.0f)\n", a.Title(), req.ID, req.Amount)
        return &ApprovalResult{
            Approved:   true,
            ApprovedBy: a.name,
        }, nil
    }
    return a.passToNext(req)
}

func (a *ManagerApprover) Title() string { return "Manager" }


// DirectorApprover approves every request — the highest level.
type DirectorApprover struct {
    BaseApprover
    name string
}

func NewDirectorApprover(name string) *DirectorApprover {
    return &DirectorApprover{name: name}
}

func (a *DirectorApprover) Approve(req ApprovalRequest) (*ApprovalResult, error) {
    fmt.Printf("[%s] Approving request %s (Rp %.0f)\n", a.Title(), req.ID, req.Amount)
    return &ApprovalResult{
        Approved:   true,
        ApprovedBy: a.name,
        Reason:     "approved by director",
    }, nil
}

func (a *DirectorApprover) Title() string { return "Director" }

// Assembling the approval chain
func buildApprovalChain() Approver {
    teamLead := NewTeamLeadApprover("Budi")
    manager  := NewManagerApprover("Citra")
    director := NewDirectorApprover("Dani")

    teamLead.SetNext(manager)
    manager.SetNext(director)

    return teamLead // the entry point is always the first handler
}

// Usage
func main() {
    chain := buildApprovalChain()

    requests := []ApprovalRequest{
        {ID: "REQ-001", Amount: 5_000_000, RequestedBy: "Eka"},    // Team Lead approves
        {ID: "REQ-002", Amount: 50_000_000, RequestedBy: "Fajar"}, // Manager approves
        {ID: "REQ-003", Amount: 500_000_000, RequestedBy: "Gita"}, // Director approves
    }

    for _, req := range requests {
        result, err := chain.Approve(req)
        if err != nil {
            fmt.Printf("Error: %v\n", err)
            continue
        }
        fmt.Printf("Result: approved=%v by=%s\n\n", result.Approved, result.ApprovedBy)
    }
}

Output:

[Team Lead] Approving request REQ-001 (Rp 5000000)
Result: approved=true by=Budi

[Team Lead] Forwarding to the next level (Rp 50000000 exceeds threshold)
[Manager] Approving request REQ-002 (Rp 50000000)
Result: approved=true by=Citra

[Team Lead] Forwarding to the next level (Rp 500000000 exceeds threshold)
[Manager] Forwarding to the next level
[Director] Approving request REQ-003 (Rp 500000000)
Result: approved=true by=Dani

Testing Chain of Responsibility #

// MockHandler for testing — records whether it was called and whether it passed on
type MockHandler struct {
    BaseHandler
    name       string
    shouldStop bool // true = stop the chain (simulating a failure)
    called     bool
}

func (h *MockHandler) Handle(req *Request, res *Response) {
    h.called = true
    if h.shouldStop {
        res.StatusCode = http.StatusUnauthorized
        res.Error = fmt.Errorf("stopped by %s", h.name)
        return
    }
    h.PassToNext(req, res)
}
func (h *MockHandler) Name() string { return h.name }


func TestChain_StopsAtFailingHandler(t *testing.T) {
    h1 := &MockHandler{name: "H1", shouldStop: false}
    h2 := &MockHandler{name: "H2", shouldStop: true} // this one fails
    h3 := &MockHandler{name: "H3", shouldStop: false}

    chain := NewChainBuilder().Add(h1, h2, h3).Build()

    req := &Request{HTTPRequest: &http.Request{Header: http.Header{}}}
    res := &Response{StatusCode: 200}
    chain.Handle(req, res)

    if !h1.called {
        t.Error("H1 should have been called")
    }
    if !h2.called {
        t.Error("H2 should have been called")
    }
    if h3.called {
        t.Error("H3 should NOT have been called after H2 stopped the chain")
    }
    if res.StatusCode != http.StatusUnauthorized {
        t.Errorf("expected 401, got %d", res.StatusCode)
    }
}

func TestChain_AllHandlersCalled_WhenAllPass(t *testing.T) {
    h1 := &MockHandler{name: "H1"}
    h2 := &MockHandler{name: "H2"}
    h3 := &MockHandler{name: "H3"}

    chain := NewChainBuilder().Add(h1, h2, h3).Build()

    req := &Request{HTTPRequest: &http.Request{Header: http.Header{}}}
    res := &Response{StatusCode: 200}
    chain.Handle(req, res)

    if !h1.called || !h2.called || !h3.called {
        t.Error("all handlers should be called when all pass")
    }
}

func TestApprovalChain_RoutesToCorrectLevel(t *testing.T) {
    chain := buildApprovalChain()

    tests := []struct {
        amount       float64
        expectedBy   string
    }{
        {5_000_000, "Budi"},    // Team Lead
        {50_000_000, "Citra"},  // Manager
        {500_000_000, "Dani"},  // Director
    }

    for _, tt := range tests {
        result, err := chain.Approve(ApprovalRequest{
            ID: "test", Amount: tt.amount, RequestedBy: "requester",
        })
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
        if !result.Approved {
            t.Errorf("expected approval for amount %.0f", tt.amount)
        }
        if result.ApprovedBy != tt.expectedBy {
            t.Errorf("expected approved by %s, got %s", tt.expectedBy, result.ApprovedBy)
        }
    }
}

When to Use and When Not to #

USE Chain of Responsibility if:
  ✓ There are several handlers that can process a request in sequence
  ✓ Which handler processes is determined at runtime based on request conditions
  ✓ You want to add, remove, or reorder handlers without modifying the client
  ✓ You are building a validation, transformation, or tiered approval pipeline
  ✓ HTTP middleware — this is the most common CoR implementation

AVOID Chain of Responsibility if:
  ✗ A single handler always processes — use Strategy
  ✗ The chain is very long (>10 handlers) — debugging becomes very hard
  ✗ Handlers need to know about other handlers — a sign of a design problem
  ✗ All handlers must be called unconditionally — use the Observer Pattern

Overly Long Chains Are Hard to Debug

If a request fails somewhere in a long chain, tracing which handler failed can take time. Every handler should log itself well (the Name() method), and there should ideally be chain-level logging that records every handler-to-handler transition. Limit a chain to around 5-7 handlers maximum for one context; if it exceeds that, consider splitting it into several sub-chains.


Chain of Responsibility Review Checklist #

DESIGN:
  □ Each handler has one clear responsibility
  □ Handlers do not depend on other handlers directly
  □ The handler order in the chain is documented and changeable without modifying handlers
  □ There is handling for the case where no handler processes the request

IMPLEMENTATION:
  □ BaseHandler avoids duplicating SetNext and PassToNext code
  □ Handlers that stop the chain do NOT call PassToNext/next
  □ Handlers that pass on ALWAYS call PassToNext/next
  □ Every handler has a Name() for logging and debugging

LOGGING:
  □ Every processing handler logs its activity
  □ Handlers that stop the chain log their reason
  □ There is chain-level logging that shows the request flow

TESTING:
  □ Test that a failing handler stops the chain
  □ Test that all handlers are called when all pass
  □ Test each concrete handler in isolation (without the chain)
  □ Test that adding a new handler does not break existing ones

Summary #

  • Chain of Responsibility flows a request through a sequence of handlers — each handler decides whether to handle, pass on, or stop the chain.
  • Two operation modes: stop-first for validation (stops at the first failing handler) and pipeline for transformation (all handlers always execute).
  • HTTP middleware is the most common CoR implementation in Gofunc(http.Handler) http.Handler is a functional handler chained with Chain().
  • BaseHandler eliminates duplicationSetNext and PassToNext are implemented once; concrete handlers only focus on their own logic.
  • Handler order is very important: Recovery → Logging → CORS → RateLimit → Auth is a different order from Auth → RateLimit → Recovery — understand the consequences of each order.
  • Approval workflows are another natural use case — requests are processed by approvers from the lowest to the highest level, stopping when someone authorized handles it.
  • Don’t make chains too long — more than 5-7 handlers in one context is hard to debug; split into sub-chains if needed.
  • Every handler must be self-contained and testable — not dependent on other handlers; it can be tested in isolation with mocks for the handlers before and after it.

← Previous: Command   Next: State →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact