☕ Analogy
You order a dark roast. Add a shot of mocha — the price goes up and the name grows. Add whip, then soy. At each step you still have “a beverage” you can ask for a description and a cost, but it’s been wrapped with one more topping. You never defined a DarkRoastWithMochaAndWhipAndSoy class — you just kept wrapping.
The problem
You want to add features to objects in arbitrary combinations. Modelling each combination as its own type is hopeless: with five condiments you’d need dozens of classes. Decorator instead makes each feature a wrapper that holds a beverage and adds to it, so combinations are built at runtime by nesting.
Structure
classDiagram
class Beverage {
<<interface>>
+Description() string
+Cost() float64
}
class DarkRoast {
+Description() string
+Cost() float64
}
class Mocha {
-component Beverage
+Description() string
+Cost() float64
}
Beverage <|.. DarkRoast
Beverage <|.. Mocha
Mocha o--> Beverage : wraps
A decorator (Mocha) both implements Beverage and holds a Beverage. That dual role is what lets you stack wrappers indefinitely.
How it works
Each layer delegates to the one it wraps, then adds its own contribution:
graph LR
D["DarkRoast<br/>$1.99"] --> M["+ Mocha<br/>+$0.20"] --> M2["+ Mocha<br/>+$0.20"] --> W["+ Whip<br/>+$0.10"] --> T["= $2.49"]
Idiomatic Go
A common version uses an explicit component Beverage field. Go’s struct embedding makes it even tighter — embed the interface and override only Description and Cost. Edit and Run:
package main
import "fmt"
type Beverage interface {
Description() string
Cost() float64
}
// Base component.
type DarkRoast struct{}
func (DarkRoast) Description() string { return "Dark Roast" }
func (DarkRoast) Cost() float64 { return 1.99 }
// Mocha embeds a Beverage and decorates it.
type Mocha struct{ Beverage }
func (m Mocha) Description() string { return m.Beverage.Description() + ", Mocha" }
func (m Mocha) Cost() float64 { return m.Beverage.Cost() + 0.20 }
// Whip is another decorator.
type Whip struct{ Beverage }
func (w Whip) Description() string { return w.Beverage.Description() + ", Whip" }
func (w Whip) Cost() float64 { return w.Beverage.Cost() + 0.10 }
func main() {
var b Beverage = DarkRoast{}
b = Mocha{b}
b = Mocha{b} // double mocha
b = Whip{b}
fmt.Printf("%s = $%.2f\n", b.Description(), b.Cost())
}
🐹 Decorators are everywhere in Go
The io package is built on decorators: gzip.NewWriter(f) wraps an io.Writer to add compression, bufio.NewWriter(gz) wraps that to add buffering — each keeps the io.Writer interface. The same idea drives HTTP middleware: func(next http.Handler) http.Handler wraps a handler with logging, auth, or recovery while still being an http.Handler.
In the standard library
compress/gzip.NewWriter,bufio.NewReader/Writer— wrap anio.Reader/io.Writer.io.MultiWriter,io.LimitReader,io.TeeReader— readers/writers that decorate others.net/httpmiddleware — handler wrappers for logging, auth, gzip, recovery.
Pitfalls
⚠️ Order matters, and depth hurts
Wrapping order changes behavior — gzip-then-buffer is not the same as buffer-then-gzip. And a tower of ten wrappers makes stack traces and debugging painful. Use decorators for genuinely composable, independent features; if the layering is fixed, a single type is clearer.
When to use it — and when not
✅ Reach for it when
- You want to add responsibilities to objects dynamically, in combinations, at runtime.
- Subclassing every combination would explode (DarkRoastWithMochaAndWhipAndSoy…).
- You want to keep the base type closed for modification but open for extension.
⛔ Think twice when
- There's exactly one fixed extension — just put it in the type.
- Deeply nested wrappers make stack traces and debugging hard to follow.
- Behavior depends on the *order* of wrapping in ways that surprise callers.
Related patterns
Treat individual objects and compositions of objects uniformly through one common interface.
STRUCTURAL AdapterConvert the interface of a type into another interface clients expect, letting otherwise-incompatible types work together.
BEHAVIORAL StrategyDefine a family of interchangeable algorithms, encapsulate each one, and select which to use at runtime.
BEHAVIORAL Chain of ResponsibilityPass a request along a chain of handlers until one of them handles it, decoupling sender from receiver.
Check your understanding
Score: 0 / 31. How does Decorator differ from Adapter?
A decorator is a wrapper that *is-a* the thing it wraps (same interface) and enriches it. An adapter is a wrapper that makes an object look like a *different* interface.
2. What Go feature makes decorators especially concise?
Embedding the wrapped interface promotes all its methods automatically, so a decorator only writes the methods it actually modifies.
3. Which is a Decorator in Go's standard library?
gzip.NewWriter wraps an io.Writer and returns an io.Writer that compresses — same interface, added behavior. bufio.NewReader and io.MultiWriter are the same idea.