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 Golang —
func(http.Handler) http.Handleradalah functional handler yang dirangkai denganChain().- BaseHandler menghilangkan duplikasi —
SetNextdanPassToNextdiimplementasikan 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.