Structural Complexity: Medium

Decorator in Go

Add behavior around an object dynamically by wrapping it with small focused layers that share the same interface.

The Problem

Cross-cutting concerns such as audit logging, secondary notifications, rate limiting, and tracing tend to pile onto otherwise simple services. If you keep modifying the base implementation for every concern, the core behavior becomes harder to test and reason about.

The Solution

Decorator wraps an object with another object that implements the same interface. In Go, decorators are usually small structs with a Next field. Each layer adds one concern before or after delegating to the wrapped object, and callers can compose only the layers they need.

Structure

Decorator Pattern
Step 1 of 5

The Component Interface

Notifier declares Send(recipient, message). Every component in the stack — base and decorators alike — implements this contract. The client only ever sees this shape.

  • Component interface: Notifier defines the behavior being extended.
  • Concrete component: EmailNotifier performs the base notification.
  • Decorators: Audit and SMS layers wrap another notifier and add behavior.
  • Client: Composes the stack explicitly at startup.

Implementation

This example starts with a plain email notifier, then wraps it with auditing and SMS delivery. Each decorator stays focused on one concern and delegates to the next layer in the chain.

package main

import "fmt"

type Notifier interface {
	Send(recipient string, message string)
}

type EmailNotifier struct{}

func (EmailNotifier) Send(recipient string, message string) {
	fmt.Printf("email to %s: %s\n", recipient, message)
}

Real-World Analogy

Think of layering clothes for cold weather. Your base outfit still does its job, but you can add a jacket, scarf, or gloves around it depending on what extra protection you need that day.

Pros and Cons

ProsCons
Adds optional behavior without changing the core component.Wrapper chains can become hard to trace and debug.
Encourages small, focused wrappers for cross-cutting concerns.The order of decorators can affect behavior in non-obvious ways.
Allows different behavior stacks in different environments.Too many tiny decorators can make simple flows feel fragmented.

Best Practices

  • Keep each decorator narrow so stacks stay understandable.
  • Prefer explicit composition in setup code over hidden automatic wrapping.
  • Make decorators preserve the same contract as the wrapped object.
  • Use decorators for additive behavior, not for changing the underlying domain meaning.
  • If wrappers start coordinating too many subsystems, you may really need a Facade or pipeline instead.

When to Use

  • You need optional layers such as logging, caching, secondary delivery, or tracing.
  • The base behavior should stay small and independently testable.
  • Different environments need different wrapper combinations.

When NOT to Use

  • The extra behavior is not optional and belongs in the core abstraction.
  • The wrapper stack would become opaque and difficult to debug.
  • A simple function call around the object would be clearer than a reusable decorator type.