Featured image

This series of articles looks at various patterns and how they apply to Go, continuing with with the GoF structural patterns.

Verdicts: Link to heading

  • Idiomatic - Considered a best practice in Go.
  • Code Smell - Not wrong, but if you are using it, make sure you understand what you are doing.
  • Anti-Pattern - Avoid unless you are an expert and know it is okay to break this rule.

Bridge Pattern Link to heading

Problem Space Link to heading

The Bridge pattern decouples an abstraction from its implementation so the two can vary independently. In classic OOP, this involves separate inheritance hierarchies connected by a “bridge.” In Go, it’s simpler: hold an interface as a field, and swap implementations as needed.

Practical Example 1 Link to heading

internal/notification/notification.go

package notification

type Sender interface {
    Send(to, message string) error
}

type Notifier struct {
    sender Sender
}

func NewNotifier(s Sender) *Notifier {
    return &Notifier{sender: s}
}

func (n *Notifier) Alert(to, message string) error {
    return n.sender.Send(to, "[ALERT] "+message)
}

func (n *Notifier) Remind(to, message string) error {
    return n.sender.Send(to, "[REMINDER] "+message)
}

internal/notification/senders.go

package notification

import "fmt"

type EmailSender struct {
    smtpHost string
}

func NewEmailSender(host string) *EmailSender {
    return &EmailSender{smtpHost: host}
}

func (e *EmailSender) Send(to, message string) error {
    fmt.Printf("Email to %s: %s\n", to, message)
    return nil
}

type SMSSender struct {
    apiKey string
}

func NewSMSSender(apiKey string) *SMSSender {
    return &SMSSender{apiKey: apiKey}
}

func (s *SMSSender) Send(to, message string) error {
    fmt.Printf("SMS to %s: %s\n", to, message)
    return nil
}

cmd/cli/main.go

package main

import "myapp/internal/notification"

func main() {
    // Same Notifier abstraction, different implementations
    emailNotifier := notification.NewNotifier(
        notification.NewEmailSender("smtp.example.com"),
    )
    emailNotifier.Alert("user@example.com", "server down")

    smsNotifier := notification.NewNotifier(
        notification.NewSMSSender("api-key-123"),
    )
    smsNotifier.Remind("555-1234", "meeting in 10 minutes")
}

Practical Example 2 Link to heading

internal/report/report.go

package report

import "io"

type Report struct {
    title   string
    content string
    writer  io.Writer
}

func New(title, content string, w io.Writer) *Report {
    return &Report{title: title, content: content, writer: w}
}

func (r *Report) Render() error {
    _, err := io.WriteString(r.writer, r.title+"\n\n"+r.content)
    return err
}

cmd/cli/main.go

package main

import (
    "bytes"
    "myapp/internal/report"
    "os"
)

func main() {
    // Same Report, different output destinations
    consoleReport := report.New("Status", "All systems operational", os.Stdout)
    consoleReport.Render()

    var buf bytes.Buffer
    bufferedReport := report.New("Status", "All systems operational", &buf)
    bufferedReport.Render()
    // buf now contains the report
}

Considerations Link to heading

Example 1: The abstraction (Notifier) holds an interface (Sender). You can add new notification types (Alert, Remind, etc.) without touching senders, and add new senders without touching the notifier.

Example 2: Using io.Writer as the bridge. The Report doesn’t care where output goes — stdout, file, buffer, network. This is Bridge at its simplest.

In Go, Bridge is just composition with interfaces. If you’re holding an interface as a struct field and injecting implementations, you’re using Bridge. It’s so idiomatic that most Go developers use it without knowing the pattern name.

Verdict Link to heading

  • Example 1: Idiomatic
  • Example 2: Idiomatic

Adapter Pattern Link to heading

Problem Space Link to heading

The Adapter pattern converts one interface into another that clients expect. It lets types work together that couldn’t otherwise because of incompatible interfaces. In Go, this is extremely common due to implicit interface satisfaction, you wrap a type to make it satisfy an interface it doesn’t natively implement.

Practical Example 1 Link to heading

cmd/server/main.go

package main

import (
    "fmt"
    "net/http"
)

func greet(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello")
}

func main() {
    // http.HandlerFunc adapts a function to satisfy http.Handler
    http.Handle("/", http.HandlerFunc(greet))
    http.ListenAndServe(":8080", nil)
}

Practical Example 2 Link to heading

internal/storage/storage.go

package storage

import (
    "bytes"
    "io"
)

// Third-party client that doesn't implement io.Reader
type BlobClient struct {
    // ...
}

func (b *BlobClient) Fetch(key string) ([]byte, error) {
    // fetch from remote storage
    return []byte("data"), nil
}

// Adapter wraps BlobClient to satisfy io.Reader
type BlobReader struct {
    client *BlobClient
    key    string
    // Embed or hold a reader. 
    // Since we fetch lazily, we hold the state in a pointer.
    reader *bytes.Reader 
}

func NewBlobReader(client *BlobClient, key string) *BlobReader {
    return &BlobReader{client: client, key: key}
}

func (r *BlobReader) Read(p []byte) (int, error) {
    if r.reader == nil {
        data, err := r.client.Fetch(r.key)
        if err != nil {
            return 0, err
        }
        // Delegate the complex offset/EOF logic to the standard library
        r.reader = bytes.NewReader(data)
    }
    return r.reader.Read(p)
}

cmd/cli/main.go

package main

import (
    "io"
    "myapp/internal/storage"
    "os"
)

func main() {
    client := &storage.BlobClient{}
    reader := storage.NewBlobReader(client, "config.json")
    
    // Now works with anything expecting io.Reader
    io.Copy(os.Stdout, reader)
}

Practical Example 3 Link to heading

internal/logger/logger.go

package logger

import "log"

// Your interface
type Logger interface {
    Info(msg string)
    Error(msg string)
}

// Adapter for standard library logger
type StdLogAdapter struct {
    logger *log.Logger
}

func NewStdLogAdapter(l *log.Logger) *StdLogAdapter {
    return &StdLogAdapter{logger: l}
}

func (a *StdLogAdapter) Info(msg string) {
    a.logger.Println("[INFO]", msg)
}

func (a *StdLogAdapter) Error(msg string) {
    a.logger.Println("[ERROR]", msg)
}

cmd/cli/main.go

package main

import (
    "log"
    "myapp/internal/logger"
    "os"
)

func main() {
    stdLogger := log.New(os.Stdout, "", log.LstdFlags)
    
    // Adapt standard logger to your interface
    var l logger.Logger = logger.NewStdLogAdapter(stdLogger)
    
    l.Info("server started")
    l.Error("connection failed")
}

Considerations Link to heading

Example 1: http.HandlerFunc is the canonical Go adapter. It’s a type conversion that adapts a function signature to satisfy the http.Handler interface. One line, zero ceremony.

Example 2: This is the standard way to fix broken or foreign interfaces. Wrap the third-party type in a struct that satisfies your interface, and delegate the work. Don’t let foreign types pollute your domain logic.

Example 3: Consumer-defined interface with an adapter for a concrete type. This keeps your code decoupled from specific implementations and makes testing easy. The Adapter pattern is so natural in Go that you often don’t realize you’re using it. Any time you wrap a type to satisfy an interface, you’re adapting.

Verdict Link to heading

  • Example 1: Idiomatic
  • Example 2: Idiomatic
  • Example 3: Idiomatic

Composite Pattern Link to heading

Problem Space Link to heading

The Composite pattern allows you to treat individual objects and collections of objects uniformly through a shared interface. This is particularly useful for tree structures where you want to perform operations across the whole hierarchy without caring whether you’re dealing with a leaf or a branch.

Practical Example 1 Link to heading

cmd/multiwriter/main.go

package main

import (
    "bytes"
    "io"
    "os"
)

func main() {
    var buf bytes.Buffer
    f, err := os.Create("log.txt")
    if err != nil {
    panic(err)
	}
    defer f.Close()

    // Composite: multiple writers treated as one
    multi := io.MultiWriter(os.Stdout, &buf, f)
    
    io.WriteString(multi, "hello\n") // writes to all three
}

Practical Example 2 Link to heading

internal/notifier/notifier.go

package notifier

type EmailNotifier struct {
    address string
}

func NewEmailNotifier(address string) *EmailNotifier {
    return &EmailNotifier{address: address}
}

func (e *EmailNotifier) Send(msg string) error {
    // send email
    return nil
}

type SMSNotifier struct {
    phone string
}

func NewSMSNotifier(phone string) *SMSNotifier {
    return &SMSNotifier{phone: phone}
}

func (s *SMSNotifier) Send(msg string) error {
    // send sms
    return nil
}

cmd/cli/main.go

package main

import (
"errors"

"myapp/internal/notifier"
)

type Sender interface {
    Send(msg string) error
}

type GroupNotifier struct {
    senders []Sender
}

func (g *GroupNotifier) Add(senders ...Sender) {
    g.senders = append(g.senders, senders...)
}

func (g *GroupNotifier) Send(msg string) error {
    var errs []error
    for _, s := range g.senders {
        errs = append(errs, s.Send(msg))
    }
    // "Composite" error handling: treat multiple errors as one
    return errors.Join(errs...)
}

func main() {
    oncall := &GroupNotifier{}
    oncall.Add(
        notifier.NewEmailNotifier("oncall@example.com"),
        notifier.NewSMSNotifier("555-1234"),
    )

    managers := &GroupNotifier{}
    managers.Add(notifier.NewEmailNotifier("boss@example.com"))


    everyone := &GroupNotifier{}
    everyone.Add(oncall, managers) 
    everyone.Send("server is down")
}

Considerations Link to heading

Example 1: io.MultiWriter returns an io.Writer that contains multiple io.Writers. The composite implements the same interface as its leaves, so callers don’t know or care that they’re writing to multiple destinations.

Example 2: The library exports concrete types. The consumer defines the interface it needs. This is the most idiomatic Go approach for behavioral dependencies.

Verdict Link to heading

  • Example 1: Idiomatic
  • Example 2: Idiomatic

Decorator Pattern Link to heading

Problem Space Link to heading

The Decorator pattern attaches additional behavior to an object dynamically without modifying its structure. Instead of inheritance, you wrap an object with another that implements the same interface and adds functionality. In Go, this is extremely common. Middleware is just decoration.

Practical Example 1 Link to heading

cmd/server/main.go

package main

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

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello"))
}

func main() {
    handler := http.HandlerFunc(hello)
    
    // Decorate: logging runs first, then auth, then hello
    decorated := logging(auth(handler))
    
    http.Handle("/", decorated)
    http.ListenAndServe(":8080", nil)
}

Practical Example 2 Link to heading

internal/storage/storage.go

package storage

import "fmt"

type Cache interface {
    Get(key string) ([]byte, error)
    Set(key string, value []byte) error
}

type memoryCache struct {
    data map[string][]byte
}

func NewMemoryCache() *memoryCache {
    return &memoryCache{data: make(map[string][]byte)}
}

func (c *memoryCache) Get(key string) ([]byte, error) {
    v, ok := c.data[key]
    if !ok {
        return nil, fmt.Errorf("key not found: %s", key)
    }
    return v, nil
}

func (c *memoryCache) Set(key string, value []byte) error {
    c.data[key] = value
    return nil
}

internal/storage/decorators.go

package storage

import (
    "log"
    "time"
)

// Logging decorator
type loggingCache struct {
    wrapped Cache
}

func WithLogging(c Cache) Cache {
    return &loggingCache{wrapped: c}
}

func (c *loggingCache) Get(key string) ([]byte, error) {
    log.Printf("cache get: %s", key)
    return c.wrapped.Get(key)
}

func (c *loggingCache) Set(key string, value []byte) error {
    log.Printf("cache set: %s", key)
    return c.wrapped.Set(key, value)
}

// Metrics decorator
type metricsCache struct {
    wrapped Cache
}

func WithMetrics(c Cache) Cache {
    return &metricsCache{wrapped: c}
}

func (c *metricsCache) Get(key string) ([]byte, error) {
    start := time.Now()
    v, err := c.wrapped.Get(key)
    log.Printf("cache.Get took %v", time.Since(start))
    return v, err
}

func (c *metricsCache) Set(key string, value []byte) error {
    start := time.Now()
    err := c.wrapped.Set(key, value)
    log.Printf("cache.Set took %v", time.Since(start))
    return err
}

cmd/cli/main.go

package main

import "myapp/internal/storage"

func main() {
    cache := storage.NewMemoryCache()
    
    // Stack decorators: metrics wraps logging wraps cache
    decorated := storage.WithMetrics(storage.WithLogging(cache))
    
    decorated.Set("user:1", []byte("alice"))
    decorated.Get("user:1")
}

Practical Example 3 Link to heading

cmd/reader/main.go

package main

import (
    "bufio"
    "compress/gzip"
    "io"
    "os"
)

func main() {
    f, err := os.Open("data.gz")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    // Stack decorators: buffered reading of gzipped file
    // f implements io.Reader
    // gzip.NewReader wraps it, returns io.Reader
    // bufio.NewReader wraps that, returns io.Reader
    gzReader, err := gzip.NewReader(f)
    if err != nil {
        panic(err)
    }
    buffered := bufio.NewReader(gzReader)

    io.Copy(os.Stdout, buffered)
}

Considerations Link to heading

Example 1: HTTP middleware is the most common Decorator in Go. Each middleware wraps the next handler, adds behavior (logging, auth, recovery), and calls the wrapped handler.

Example 2: Consumer-defined interface with stackable decorators. Each decorator implements the same interface and wraps another implementation. You can compose them in any order.

Example 3: The standard library uses this pattern extensively. bufio.Reader, gzip.Reader, io.TeeReader, io.LimitReader are all decorators that wrap io.Reader and add behavior.

The key insight: if you’re wrapping an interface implementation with another implementation of the same interface to add behavior, you’re using Decorator.

Verdict Link to heading

  • Example 1: Idiomatic
  • Example 2: Idiomatic
  • Example 3: Idiomatic

Facade Pattern Link to heading

Problem Space Link to heading

The Facade pattern provides a simplified interface to a complex subsystem. Instead of exposing multiple components and forcing the caller to coordinate them, you provide a single entry point that handles the orchestration internally.

Practical Example 1 Link to heading

cmd/client/main.go

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    // http.Get is a facade over:
    // - creating a Client
    // - creating a Request
    // - setting headers
    // - executing the request
    // - handling redirects
    resp, err := http.Get("https://example.com")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}

Practical Example 2 Link to heading

internal/order/order.go

package order

import "fmt"

// Subsystem: Inventory
type inventory struct{}

func (i *inventory) Check(productID string, qty int) bool {
    fmt.Printf("checking inventory for %s\n", productID)
    return true
}

func (i *inventory) Reserve(productID string, qty int) error {
    fmt.Printf("reserving %d of %s\n", qty, productID)
    return nil
}

// Subsystem: Payment
type payment struct{}

func (p *payment) Authorize(amount float64) (string, error) {
    fmt.Printf("authorizing payment of $%.2f\n", amount)
    return "txn-123", nil
}

func (p *payment) Capture(txnID string) error {
    fmt.Printf("capturing transaction %s\n", txnID)
    return nil
}

// Subsystem: Shipping
type shipping struct{}

func (s *shipping) CreateLabel(address string) (string, error) {
    fmt.Printf("creating shipping label for %s\n", address)
    return "tracking-456", nil
}

// Facade
type OrderService struct {
    inventory *inventory
    payment   *payment
    shipping  *shipping
}

func NewOrderService() *OrderService {
    return &OrderService{
        inventory: &inventory{},
        payment:   &payment{},
        shipping:  &shipping{},
    }
}

func (o *OrderService) PlaceOrder(productID string, qty int, amount float64, address string) (string, error) {
    if !o.inventory.Check(productID, qty) {
        return "", fmt.Errorf("insufficient inventory")
    }
    
    if err := o.inventory.Reserve(productID, qty); err != nil {
        return "", err
    }
    
    txnID, err := o.payment.Authorize(amount)
    if err != nil {
        return "", err
    }
    
    if err := o.payment.Capture(txnID); err != nil {
        return "", err
    }
    
    trackingID, err := o.shipping.CreateLabel(address)
    if err != nil {
        return "", err
    }
    
    return trackingID, nil
}

cmd/cli/main.go

package main

import (
    "fmt"
    "myapp/internal/order"
)

func main() {
    // Caller doesn't need to know about inventory, payment, or shipping
    svc := order.NewOrderService()
    
    trackingID, err := svc.PlaceOrder("SKU-001", 2, 99.99, "123 Main St")
    if err != nil {
        panic(err)
    }
    
    fmt.Printf("order placed, tracking: %s\n", trackingID)
}

Considerations Link to heading

Example 1: http.Get is a facade over the entire HTTP client subsystem. One function call hides client creation, request construction, and execution. The standard library is full of these convenience functions.

Example 2: The order service coordinates three subsystems. Callers don’t need to understand the sequence of inventory checks, payment authorization, and shipping label creation. The facade handles orchestration.

Facade is less about interface satisfaction and more about API design. If you’re hiding complexity behind a simpler API, you’re using Facade.

Verdict Link to heading

  • Example 1: Idiomatic
  • Example 2: Idiomatic

Flyweight Pattern Link to heading

Problem Space Link to heading

The Flyweight pattern minimizes memory usage by sharing common state between multiple objects. Instead of each object storing its own copy of repeated data, they share a reference to a single instance. This is useful when you have many objects with overlapping data.

Practical Example 1 Link to heading

cmd/pool/main.go

package main

import (
    "bytes"
    "sync"
)

var bufferPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer)
    },
}

func process(data string) string {
    // Get a buffer from the pool (shared, reused)
    buf := bufferPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufferPool.Put(buf)
    
    buf.WriteString("processed: ")
    buf.WriteString(data)
    
    return buf.String()
}

func main() {
    // Each call reuses buffers instead of allocating new ones
    result1 := process("first")
    result2 := process("second")
    result3 := process("third")
    
    println(result1)
    println(result2)
    println(result3)
}

Practical Example 2 Link to heading

internal/game/sprite.go

package game

import "fmt"

// Shared intrinsic state (the flyweight)
type Sprite struct {
    texture    []byte // large, shared across all instances
    width      int
    height     int
}

var spriteCache = make(map[string]*Sprite)

func GetSprite(name string) *Sprite {
    if s, ok := spriteCache[name]; ok {
        return s
    }
    // Load once, share forever
    s := &Sprite{
        texture: loadTexture(name), // expensive
        width:   64,
        height:  64,
    }
    spriteCache[name] = s
    return s
}

func loadTexture(name string) []byte {
    fmt.Printf("loading texture: %s\n", name)
    return make([]byte, 1024*1024) // 1MB texture
}

// Extrinsic state (unique per instance)
type Enemy struct {
    sprite *Sprite // shared
    x, y   float64 // unique per enemy
    health int     // unique per enemy
}

func NewEnemy(spriteName string, x, y float64) *Enemy {
    return &Enemy{
        sprite: GetSprite(spriteName), // shared flyweight
        x:      x,
        y:      y,
        health: 100,
    }
}

cmd/game/main.go

package main

import "myapp/internal/game"

func main() {
    // 1000 enemies, but only one "goblin" texture in memory
    enemies := make([]*game.Enemy, 1000)
    for i := range enemies {
        enemies[i] = game.NewEnemy("goblin", float64(i*10), float64(i*5))
    }
    
    // Only loads texture once
    boss := game.NewEnemy("goblin", 500, 500)
    _ = boss
}

Considerations Link to heading

Example 1: sync.Pool is the standard library’s flyweight for temporary objects. Instead of allocating and garbage collecting buffers constantly, you reuse them. This is common in high-throughput servers.

Example 2: Game sprites separate intrinsic state (texture data shared by all goblins) from extrinsic state (position and health unique to each enemy). A thousand enemies share one texture. In a real application with concurrent access you would want to add in a sync.RWMutex or sync.Map.

The pattern splits object state into intrinsic (shared, immutable) and extrinsic (unique, per-instance). The cache or pool manages the shared instances.

Verdict Link to heading

  • Example 1: Idiomatic
  • Example 2: Idiomatic

Proxy Pattern Link to heading

Problem Space Link to heading

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Common uses include lazy initialization, access control, logging, and caching. The proxy implements the same interface as the real object, so callers don’t know they’re talking to a proxy.

Practical Example 1 Link to heading

internal/database/database.go

package database

import (
    "database/sql"
    "fmt"
    "sync"
)

type DB interface {
    Query(query string, args ...any) (*sql.Rows, error)
    Close() error
}

// Real implementation
type realDB struct {
    conn *sql.DB
}

func newRealDB(dsn string) (*realDB, error) {
    conn, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, err
    }
    return &realDB{conn: conn}, nil
}

func (db *realDB) Query(query string, args ...any) (*sql.Rows, error) {
    return db.conn.Query(query, args...)
}

func (db *realDB) Close() error {
    return db.conn.Close()
}

// Lazy proxy - only connects when first used
type LazyDB struct {
    dsn     string
    real    *realDB
    once    sync.Once
    initErr error
}

func NewLazyDB(dsn string) *LazyDB {
    return &LazyDB{dsn: dsn}
}

func (db *LazyDB) init() {
    db.once.Do(func() {
        fmt.Println("connecting to database...")
        db.real, db.initErr = newRealDB(db.dsn)
    })
}

func (db *LazyDB) Query(query string, args ...any) (*sql.Rows, error) {
    db.init()
    if db.initErr != nil {
        return nil, db.initErr
    }
    return db.real.Query(query, args...)
}

func (db *LazyDB) Close() error {
    if db.real != nil {
        return db.real.Close()
    }
    return nil
}

cmd/cli/main.go

package main

import "myapp/internal/database"

func main() {
    // No connection made yet
    db := database.NewLazyDB("postgres://localhost/myapp")
    defer db.Close()
    
    // Connection happens here, on first use
    rows, err := db.Query("SELECT id FROM users")
    if err != nil {
        panic(err)
    }
    defer rows.Close()
}

Practical Example 2 Link to heading

internal/api/api.go

package api

import "fmt"

type Service interface {
    GetData(id string) (string, error)
    DeleteData(id string) error
}

// Real implementation
type realService struct{}

func NewService() *realService {
    return &realService{}
}

func (s *realService) GetData(id string) (string, error) {
    return fmt.Sprintf("data for %s", id), nil
}

func (s *realService) DeleteData(id string) error {
    fmt.Printf("deleted %s\n", id)
    return nil
}

// Access control proxy
type SecureService struct {
    real Service
    role string
}

func NewSecureService(real Service, role string) *SecureService {
    return &SecureService{real: real, role: role}
}

func (s *SecureService) GetData(id string) (string, error) {
    // Everyone can read
    return s.real.GetData(id)
}

func (s *SecureService) DeleteData(id string) error {
    // Only admins can delete
    if s.role != "admin" {
        return fmt.Errorf("permission denied: %s cannot delete", s.role)
    }
    return s.real.DeleteData(id)
}

cmd/cli/main.go

package main

import (
    "fmt"
    "myapp/internal/api"
)

func main() {
    svc := api.NewService()
    
    // User proxy
    userSvc := api.NewSecureService(svc, "user")
    data, _ := userSvc.GetData("123")
    fmt.Println(data)
    
    err := userSvc.DeleteData("123")
    fmt.Println(err) // permission denied
    
    // Admin proxy
    adminSvc := api.NewSecureService(svc, "admin")
    err = adminSvc.DeleteData("123")
    fmt.Println(err) // nil
}

Considerations Link to heading

Example 1: Lazy proxy defers expensive initialization until the object is actually used. Useful for database connections, file handles, or any resource you might not need on every code path.

Example 2: Protection proxy adds access control without modifying the real implementation. The proxy checks permissions before delegating to the wrapped service.

Proxy is similar to Decorator, but the intent differs. Decorator adds behavior. Proxy controls access. In practice, the implementation often looks the same: wrap an interface, delegate calls.

Verdict Link to heading

  • Example 1: Idiomatic
  • Example 2: Idiomatic