What is Decorator Pattern?

The Decorator pattern is a structural design pattern that allows you to add new functionality to objects dynamically without altering their structure. It provides a flexible alternative to subclassing for extending functionality. Think of it like adding layers of clothing - each layer adds functionality (warmth, style, protection) while keeping the core person unchanged.

I’ll show you how this pattern can help you build flexible, composable systems in Go that follow the open/closed principle.

Let’s start with a scenario: HTTP Middleware

Imagine you’re building a web API that needs various middleware functionalities: logging, authentication, rate limiting, compression, and CORS handling. You want to be able to mix and match these features without creating a massive monolithic handler.

Without Decorator Pattern

Here’s how you might handle this without the Decorator pattern:

type APIHandler struct {
    enableLogging     bool
    enableAuth        bool
    enableRateLimit   bool
    enableCompression bool
    enableCORS        bool
}

func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // All functionality crammed into one method
    if h.enableLogging {
        log.Printf("Request: %s %s", r.Method, r.URL.Path)
    }
    
    if h.enableCORS {
        w.Header().Set("Access-Control-Allow-Origin", "*")
    }
    
    if h.enableAuth {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
    }
    
    if h.enableRateLimit {
        // Rate limiting logic
        if !checkRateLimit(r.RemoteAddr) {
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
    }
    
    // Actual business logic
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Hello, World!"))
    
    if h.enableCompression {
        // Compression logic (too late here!)
    }
}

// This approach becomes unwieldy quickly

With Decorator Pattern

Let’s refactor this using the Decorator pattern:

package main

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

// Component interface
type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request)
}

// Concrete component
type BaseHandler struct {
    message string
}

func (h *BaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(h.message))
}

// Base decorator
type HandlerDecorator struct {
    handler Handler
}

func (d *HandlerDecorator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    d.handler.ServeHTTP(w, r)
}

// Concrete decorators
type LoggingDecorator struct {
    HandlerDecorator
}

func NewLoggingDecorator(handler Handler) *LoggingDecorator {
    return &LoggingDecorator{
        HandlerDecorator: HandlerDecorator{handler: handler},
    }
}

func (d *LoggingDecorator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    log.Printf("Started %s %s", r.Method, r.URL.Path)
    
    d.handler.ServeHTTP(w, r)
    
    duration := time.Since(start)
    log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, duration)
}

type AuthDecorator struct {
    HandlerDecorator
    validTokens map[string]bool
}

func NewAuthDecorator(handler Handler, tokens []string) *AuthDecorator {
    validTokens := make(map[string]bool)
    for _, token := range tokens {
        validTokens[token] = true
    }
    
    return &AuthDecorator{
        HandlerDecorator: HandlerDecorator{handler: handler},
        validTokens:      validTokens,
    }
}

func (d *AuthDecorator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    authHeader := r.Header.Get("Authorization")
    if authHeader == "" {
        http.Error(w, "Authorization header required", http.StatusUnauthorized)
        return
    }
    
    token := strings.TrimPrefix(authHeader, "Bearer ")
    if !d.validTokens[token] {
        http.Error(w, "Invalid token", http.StatusUnauthorized)
        return
    }
    
    d.handler.ServeHTTP(w, r)
}

type RateLimitDecorator struct {
    HandlerDecorator
    requests map[string][]time.Time
    limit    int
    window   time.Duration
}

func NewRateLimitDecorator(handler Handler, limit int, window time.Duration) *RateLimitDecorator {
    return &RateLimitDecorator{
        HandlerDecorator: HandlerDecorator{handler: handler},
        requests:         make(map[string][]time.Time),
        limit:            limit,
        window:           window,
    }
}

func (d *RateLimitDecorator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    clientIP := r.RemoteAddr
    now := time.Now()
    
    // Clean old requests
    if requests, exists := d.requests[clientIP]; exists {
        var validRequests []time.Time
        for _, reqTime := range requests {
            if now.Sub(reqTime) < d.window {
                validRequests = append(validRequests, reqTime)
            }
        }
        d.requests[clientIP] = validRequests
    }
    
    // Check rate limit
    if len(d.requests[clientIP]) >= d.limit {
        http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
        return
    }
    
    // Add current request
    d.requests[clientIP] = append(d.requests[clientIP], now)
    
    d.handler.ServeHTTP(w, r)
}

type CORSDecorator struct {
    HandlerDecorator
    allowedOrigins []string
}

func NewCORSDecorator(handler Handler, origins []string) *CORSDecorator {
    return &CORSDecorator{
        HandlerDecorator: HandlerDecorator{handler: handler},
        allowedOrigins:   origins,
    }
}

func (d *CORSDecorator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    origin := r.Header.Get("Origin")
    
    // Check if origin is allowed
    allowed := false
    for _, allowedOrigin := range d.allowedOrigins {
        if allowedOrigin == "*" || allowedOrigin == origin {
            allowed = true
            break
        }
    }
    
    if allowed {
        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", "Content-Type, Authorization")
    }
    
    if r.Method == "OPTIONS" {
        w.WriteHeader(http.StatusOK)
        return
    }
    
    d.handler.ServeHTTP(w, r)
}

func main() {
    // Create base handler
    baseHandler := &BaseHandler{message: "Hello, World!"}
    
    // Compose decorators
    handler := NewLoggingDecorator(
        NewAuthDecorator(
            NewRateLimitDecorator(
                NewCORSDecorator(
                    baseHandler,
                    []string{"*"},
                ),
                10,                // 10 requests
                time.Minute,       // per minute
            ),
            []string{"valid-token-123", "admin-token-456"},
        ),
    )
    
    // Different composition for different endpoints
    publicHandler := NewLoggingDecorator(
        NewCORSDecorator(
            &BaseHandler{message: "Public endpoint"},
            []string{"*"},
        ),
    )
    
    http.Handle("/api/protected", handler)
    http.Handle("/api/public", publicHandler)
    
    fmt.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Advanced Decorator with Configuration

For more complex scenarios, you can create configurable decorators:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

// Decorator configuration
type DecoratorConfig struct {
    Type   string                 `json:"type"`
    Config map[string]interface{} `json:"config"`
}

// Decorator factory
type DecoratorFactory struct {
    builders map[string]func(Handler, map[string]interface{}) Handler
}

func NewDecoratorFactory() *DecoratorFactory {
    factory := &DecoratorFactory{
        builders: make(map[string]func(Handler, map[string]interface{}) Handler),
    }
    
    // Register decorator builders
    factory.Register("logging", func(h Handler, config map[string]interface{}) Handler {
        return NewLoggingDecorator(h)
    })
    
    factory.Register("auth", func(h Handler, config map[string]interface{}) Handler {
        tokens, _ := config["tokens"].([]interface{})
        var tokenStrings []string
        for _, token := range tokens {
            tokenStrings = append(tokenStrings, token.(string))
        }
        return NewAuthDecorator(h, tokenStrings)
    })
    
    factory.Register("ratelimit", func(h Handler, config map[string]interface{}) Handler {
        limit := int(config["limit"].(float64))
        windowSec := int(config["window_seconds"].(float64))
        return NewRateLimitDecorator(h, limit, time.Duration(windowSec)*time.Second)
    })
    
    factory.Register("cors", func(h Handler, config map[string]interface{}) Handler {
        origins, _ := config["origins"].([]interface{})
        var originStrings []string
        for _, origin := range origins {
            originStrings = append(originStrings, origin.(string))
        }
        return NewCORSDecorator(h, originStrings)
    })
    
    return factory
}

func (f *DecoratorFactory) Register(name string, builder func(Handler, map[string]interface{}) Handler) {
    f.builders[name] = builder
}

func (f *DecoratorFactory) Build(handler Handler, configs []DecoratorConfig) Handler {
    result := handler
    
    // Apply decorators in order
    for _, config := range configs {
        if builder, exists := f.builders[config.Type]; exists {
            result = builder(result, config.Config)
        }
    }
    
    return result
}

// Configuration-driven handler creation
func CreateHandlerFromConfig(baseHandler Handler, configJSON string) (Handler, error) {
    var configs []DecoratorConfig
    if err := json.Unmarshal([]byte(configJSON), &configs); err != nil {
        return nil, err
    }
    
    factory := NewDecoratorFactory()
    return factory.Build(baseHandler, configs), nil
}

func main() {
    baseHandler := &BaseHandler{message: "Configured endpoint"}
    
    // Configuration as JSON
    configJSON := `[
        {
            "type": "cors",
            "config": {
                "origins": ["*"]
            }
        },
        {
            "type": "ratelimit",
            "config": {
                "limit": 5,
                "window_seconds": 60
            }
        },
        {
            "type": "auth",
            "config": {
                "tokens": ["secret-token"]
            }
        },
        {
            "type": "logging",
            "config": {}
        }
    ]`
    
    handler, err := CreateHandlerFromConfig(baseHandler, configJSON)
    if err != nil {
        panic(err)
    }
    
    http.Handle("/api/configured", handler)
    
    fmt.Println("Server starting on :8080")
    fmt.Println("Try: curl -H 'Authorization: Bearer secret-token' http://localhost:8080/api/configured")
}

Real-world Use Cases

Here’s where I commonly use the Decorator pattern in Go:

  1. HTTP Middleware: Adding cross-cutting concerns like logging, auth, metrics
  2. Database Connections: Adding retry logic, connection pooling, query logging
  3. File Operations: Adding compression, encryption, caching layers
  4. API Clients: Adding retry logic, rate limiting, request/response transformation
  5. Stream Processing: Adding filtering, transformation, buffering to data streams

Benefits of Decorator Pattern

  1. Flexibility: Add or remove functionality at runtime
  2. Single Responsibility: Each decorator has one specific purpose
  3. Composability: Combine decorators in different ways
  4. Open/Closed Principle: Extend functionality without modifying existing code
  5. Reusability: Decorators can be reused across different components

Caveats

While the Decorator pattern is powerful, consider these limitations:

  1. Complexity: Can create deeply nested object structures
  2. Performance: Multiple layers can add overhead
  3. Debugging: Stack traces can become difficult to follow
  4. Order Dependency: The order of decorators often matters
  5. Interface Bloat: All decorators must implement the same interface

Thank you

Thank you for reading! The Decorator pattern is incredibly useful for building flexible, composable systems in Go. It’s especially powerful when combined with Go’s interfaces and the middleware pattern commonly used in web frameworks. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!