Chain of Responsibility Pattern #

Sebuah request HTTP masuk ke server. Sebelum mencapai handler bisnis, ia harus melewati beberapa pemeriksaan: apakah ada JWT token yang valid? Apakah user ini sudah terverifikasi emailnya? Apakah rate limit belum terlampaui? Apakah request body tidak melebihi batas ukuran? Tanpa Chain of Responsibility, semua pengecekan ini berakhir dalam satu fungsi besar yang sulit di-test dan sulit diubah urutannya. Dengan Chain of Responsibility, setiap pengecekan adalah handler mandiri yang bisa berdiri sendiri, dan handler-handler itu dirangkai secara berurutan — jika satu handler gagal, ia memutus rantai dan mengembalikan error; jika lolos, request diteruskan ke handler berikutnya. Menambah pemeriksaan baru berarti menambah satu handler dan menyisipkannya di posisi yang tepat dalam rantai — tanpa mengubah handler lain sama sekali.

Apa itu Chain of Responsibility? #

Chain of Responsibility (CoR) adalah behavioral design pattern yang mengalirkan request melalui serangkaian handler secara berurutan, di mana setiap handler memutuskan: tangani request ini, teruskan ke handler berikutnya, atau hentikan rantai dengan error.

Ada dua mode operasi yang berbeda dalam Chain of Responsibility:

  • Stop-first — handler menghentikan rantai begitu ada yang menangani atau gagal; cocok untuk validasi dan autentikasi
  • Pipeline — setiap handler memproses request dan meneruskannya; semua handler selalu dieksekusi; cocok untuk transformasi dan enrichment data

Perbedaan ini penting karena menentukan bagaimana rantai berperilaku ketika handler “menangani” sebuah request.

flowchart LR
    subgraph "Stop-first (Validasi)"
        R1[Request] --> H1A[Auth\nHandler]
        H1A -->|"lolos"| H2A[Rate\nLimit]
        H2A -->|"lolos"| H3A[Business\nHandler]
        H1A -->|"gagal → berhenti"| E1[Error 401]
        H2A -->|"gagal → berhenti"| E2[Error 429]
    end

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

Masalah yang Dipecahkan #

Chain of Responsibility menyelesaikan masalah yang sangat spesifik: bagaimana mengorganisir serangkaian penanganan yang berurutan tanpa hardcode urutan dan logikanya di satu tempat.

Masalah: Semua Validasi Tertumpuk di Satu Tempat #

// ANTI-PATTERN: semua validasi dalam satu fungsi — sulit di-test dan sulit diubah urutan
func HandleRequest(w http.ResponseWriter, r *http.Request) {
    // Validasi 1: autentikasi
    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
    }

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

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

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

    // Baru sekarang bisnis logic
    actualHandler(w, r)
    // Menambah validasi baru berarti masuk ke sini dan mengetik kode baru
}

// BENAR: setiap validasi adalah handler mandiri yang bisa dirangkai
chain := BuildChain(
    NewAuthHandler(jwtSvc),
    NewEmailVerifiedHandler(),
    NewRateLimitHandler(rateLimiter),
    NewBodySizeHandler(1<<20),
)
chain.Handle(w, r)
// Menambah validasi baru = buat handler baru, sisipkan ke chain

Dua Struktur Handler di Golang #

Golang memiliki dua cara idiomatik mengimplementasikan Chain of Responsibility, masing-masing cocok untuk konteks yang berbeda.

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\nmemanggil next di dalam"]
        F2["Dirangkai dengan wrap:\nC(B(A(base)))"]
    end
Pendekatan Cara Kerja Cocok Untuk
Struct-based Handler menyimpan referensi ke next; memanggil next.Handle() secara eksplisit Request yang bisa berhenti di tengah (validasi)
Functional middleware Fungsi menerima next dan mengembalikan handler baru; selalu memanggil next kecuali error HTTP middleware, gRPC interceptor

Implementasi Lengkap: HTTP Middleware Chain #

Struct-based Handler Interface #

package middleware

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

// Request membawa semua data yang mengalir melalui rantai handler.
// Field Context memungkinkan handler menulis data untuk handler berikutnya.
type Request struct {
    HTTPRequest *http.Request
    Context     context.Context
    UserID      string
    Claims      map[string]interface{}
    StartTime   time.Time
}

// Response membawa hasil yang dibangun oleh handler.
type Response struct {
    StatusCode int
    Body       interface{}
    Headers    map[string]string
    Error      error
}

// Handler adalah interface untuk semua handler dalam rantai.
type Handler interface {
    Handle(req *Request, res *Response)
    SetNext(handler Handler)
    Name() string
}

// BaseHandler menyediakan implementasi default untuk SetNext dan Handle.
// Di-embed oleh semua concrete handler untuk menghindari duplikasi.
type BaseHandler struct {
    next Handler
}

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

// PassToNext meneruskan request ke handler berikutnya dalam rantai.
// Dipanggil oleh concrete handler setelah berhasil memproses 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 memvalidasi JWT token dalam 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 // hentikan rantai — tidak memanggil 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
    }

    // Tulis hasil ke request context untuk digunakan handler berikutnya
    req.UserID = claims["sub"].(string)
    req.Claims = claims

    h.PassToNext(req, res) // lolos — teruskan ke handler berikutnya
}

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


// EmailVerifiedHandler memastikan user sudah memverifikasi emailnya.
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 membatasi jumlah request per user per menit.
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 menolak request dengan body terlalu besar.
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 mencatat setiap request — selalu meneruskan ke handler berikutnya.
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) // teruskan dulu, log setelah selesai
    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 memudahkan perakitan rantai handler.
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 merangkai semua handler menjadi linked list dan mengembalikan handler pertama.
func (b *ChainBuilder) Build() Handler {
    if len(b.handlers) == 0 {
        return nil
    }
    // Rangkai dari belakang ke depan
    for i := len(b.handlers) - 2; i >= 0; i-- {
        b.handlers[i].SetNext(b.handlers[i+1])
    }
    return b.handlers[0]
}

// Penggunaan
func buildAPIChain(jwtSecret string, rateLimiter *RateLimitHandler) Handler {
    return NewChainBuilder().
        Add(NewLoggingHandler(logger)).         // 1. log semua request
        Add(NewAuthHandler(jwtSecret)).          // 2. validasi token
        Add(NewEmailVerifiedHandler()).           // 3. cek email terverifikasi
        Add(NewRateLimitHandler(100)).           // 4. cek rate limit
        Add(NewBodySizeHandler(1 << 20)).        // 5. cek ukuran body
        Build()
}

Functional Middleware: Gaya HTTP Idiomatic #

Di Golang, HTTP middleware adalah implementasi CoR yang paling natural — setiap middleware adalah fungsi yang menerima http.Handler dan mengembalikan http.Handler.

// Middleware adalah function type untuk HTTP middleware
type Middleware func(http.Handler) http.Handler

// Chain merangkai beberapa middleware menjadi satu handler.
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
    // Terapkan dalam urutan terbalik agar eksekusi berjalan kiri ke kanan
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

// LoggingMiddleware mencatat setiap request dan durasinya.
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()
            // Bungkus ResponseWriter untuk menangkap status code
            wrapped := &statusRecorder{ResponseWriter: w, statusCode: 200}

            next.ServeHTTP(wrapped, r) // teruskan ke handler berikutnya

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

// statusRecorder menangkap status code yang di-write ke ResponseWriter
type statusRecorder struct {
    http.ResponseWriter
    statusCode int
}

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

// AuthMiddleware memvalidasi JWT dan menyisipkan user info ke 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 // hentikan rantai — tidak memanggil next
            }

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

            // Sisipkan claims ke context untuk handler berikutnya
            ctx := context.WithValue(r.Context(), contextKeyUserID, claims["sub"])
            next.ServeHTTP(w, r.WithContext(ctx)) // teruskan dengan context baru
        })
    }
}

// RateLimitMiddleware membatasi request 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 menangkap panic dan mengubahnya menjadi 500 error.
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 menambahkan 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)
        })
    }
}

// Perakitan — urutan dari luar ke dalam
func setupRouter() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/orders", handleOrders)
    mux.HandleFunc("/api/users", handleUsers)

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

Pipeline: Semua Handler Selalu Dieksekusi #

Mode pipeline berbeda dari stop-first — setiap handler memproses dan meneruskan request, membangun hasil secara bertahap. Berguna untuk transformasi data.

// DataPipeline memproses data melalui serangkaian transformer berurutan.
type DataTransformer interface {
    Transform(data map[string]interface{}) (map[string]interface{}, error)
    Name() string
}

// Pipeline menjalankan semua transformer secara berurutan.
// Berbeda dari CoR stop-first — semua transformer selalu dieksekusi.
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
}

// Contoh transformer konkret
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" }

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

Studi Kasus: Approval Workflow #

Chain of Responsibility juga sangat cocok untuk sistem persetujuan berjenjang — request diproses oleh approver yang berbeda berdasarkan nilai atau tipe request.

// ApprovalRequest merepresentasikan permintaan yang butuh persetujuan.
type ApprovalRequest struct {
    ID          string
    Type        string
    Amount      float64
    RequestedBy string
    Description string
}

// ApprovalResult menyimpan hasil dari proses approval.
type ApprovalResult struct {
    Approved   bool
    ApprovedBy string
    Reason     string
}

// Approver adalah handler untuk approval workflow.
type Approver interface {
    Approve(req ApprovalRequest) (*ApprovalResult, error)
    SetNext(approver Approver)
    Title() string
}

// BaseApprover menyediakan implementasi SetNext.
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 menyetujui request sampai 10 juta.
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] Menyetujui 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] Meneruskan ke level berikutnya (Rp %.0f melebihi threshold)\n",
        a.Title(), req.Amount)
    return a.passToNext(req)
}

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


// ManagerApprover menyetujui request sampai 100 juta.
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] Menyetujui 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 menyetujui semua request — level tertinggi.
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] Menyetujui 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" }

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

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

    return teamLead // titik masuk selalu handler pertama
}

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

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

    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] Menyetujui request REQ-001 (Rp 5000000)
Result: approved=true by=Budi

[Team Lead] Meneruskan ke level berikutnya (Rp 50000000 melebihi threshold)
[Manager] Menyetujui request REQ-002 (Rp 50000000)
Result: approved=true by=Citra

[Team Lead] Meneruskan ke level berikutnya (Rp 500000000 melebihi threshold)
[Manager] Meneruskan ke level berikutnya
[Director] Menyetujui request REQ-003 (Rp 500000000)
Result: approved=true by=Dani

Testing Chain of Responsibility #

// MockHandler untuk testing — mencatat apakah dipanggil dan apakah meneruskan
type MockHandler struct {
    BaseHandler
    name       string
    shouldStop bool // true = hentikan rantai (simulasi gagal)
    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} // ini yang gagal
    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)
        }
    }
}

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Chain of Responsibility jika:
  ✓ Ada beberapa handler yang bisa memproses request secara berurutan
  ✓ Handler yang memproses ditentukan saat runtime berdasarkan kondisi request
  ✓ Ingin menambah, menghapus, atau mengubah urutan handler tanpa memodifikasi client
  ✓ Membangun pipeline validasi, transformasi, atau approval berjenjang
  ✓ HTTP middleware — ini adalah implementasi CoR yang paling umum

HINDARI Chain of Responsibility jika:
  ✗ Selalu satu handler yang memproses — gunakan Strategy
  ✗ Rantai sangat panjang (>10 handler) — debugging menjadi sangat sulit
  ✗ Handler perlu tahu tentang handler lain — ini sinyal masalah desain
  ✗ Semua handler harus dipanggil tanpa kondisi — gunakan Observer Pattern

Rantai yang Terlalu Panjang Sulit Di-debug

Jika request gagal di suatu titik dalam rantai yang panjang, menelusuri handler mana yang gagal bisa memakan waktu. Setiap handler harus mencatat dirinya dengan baik (Name() method), dan sebaiknya ada logging di level chain yang mencatat setiap transisi antar handler. Batasi rantai di sekitar 5-7 handler maksimum untuk satu konteks; jika lebih, pertimbangkan untuk memecah menjadi beberapa sub-chain.


Checklist Review Chain of Responsibility #

DESAIN:
  □ Setiap handler memiliki satu tanggung jawab yang jelas
  □ Handler tidak bergantung pada handler lain secara langsung
  □ Urutan handler dalam rantai didokumentasikan dan bisa diubah tanpa mengubah handler
  □ Ada penanganan untuk kasus request tidak ditangani siapapun

IMPLEMENTASI:
  □ BaseHandler menghindari duplikasi kode SetNext dan PassToNext
  □ Handler yang menghentikan rantai TIDAK memanggil PassToNext/next
  □ Handler yang meneruskan SELALU memanggil PassToNext/next
  □ Setiap handler memiliki Name() untuk logging dan debugging

LOGGING:
  □ Setiap handler yang memproses mencatat aktivitasnya
  □ Handler yang menghentikan rantai mencatat alasannya
  □ Ada log di level chain yang menunjukkan alur request

TESTING:
  □ Test bahwa handler yang gagal menghentikan rantai
  □ Test bahwa semua handler dipanggil ketika semua pass
  □ Test setiap concrete handler secara terisolasi (tanpa chain)
  □ Test bahwa penambahan handler baru tidak merusak handler yang sudah ada

Ringkasan #

  • Chain of Responsibility mengalirkan request melalui serangkaian handler — setiap handler memutuskan apakah menangani, meneruskan, atau menghentikan rantai.
  • Dua mode operasi: stop-first untuk validasi (berhenti di handler pertama yang gagal) dan pipeline untuk transformasi (semua handler selalu dieksekusi).
  • HTTP middleware adalah implementasi CoR yang paling umum di Golangfunc(http.Handler) http.Handler adalah functional handler yang dirangkai dengan Chain().
  • BaseHandler menghilangkan duplikasiSetNext dan PassToNext diimplementasikan sekali; concrete handler hanya fokus pada logikanya sendiri.
  • Urutan handler sangat penting: Recovery → Logging → CORS → RateLimit → Auth adalah urutan yang berbeda dari Auth → RateLimit → Recovery — pahami konsekuensi setiap urutan.
  • Approval workflow adalah use case lain yang natural — request diproses oleh approver dari level terendah ke tertinggi, berhenti ketika ada yang berwenang.
  • Jangan buat rantai terlalu panjang — lebih dari 5-7 handler dalam satu konteks sulit di-debug; pecah menjadi sub-chain jika perlu.
  • Setiap handler harus mandiri dan testable — tidak bergantung pada handler lain; bisa di-test secara terisolasi dengan mock untuk handler sebelum dan sesudahnya.

← Sebelumnya: Command   Berikutnya: State →

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