☕ 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 : wrapsA 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.
Decorator as HTTP middleware
The most common Decorator you’ll write in Go is middleware: a func(http.Handler) http.Handler that wraps a handler with logging, auth, or recovery while still being an http.Handler. Stacking them is decorator nesting. This runs in-process (no real network needed):
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
// A Decorator: take a Handler, return a Handler with extra behavior.
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("->", r.Method, r.URL.Path)
next.ServeHTTP(w, r) // delegate to the wrapped handler
fmt.Println("<- done")
})
}
func AddHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Demo", "yes")
next.ServeHTTP(w, r)
})
}
func main() {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
})
h = AddHeader(h) // inner
h = Logging(h) // outer — runs first
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/hi", nil))
fmt.Println("status:", rec.Code, "X-Demo:", rec.Header().Get("X-Demo"))
}
Each layer keeps the http.Handler contract and adds one concern — exactly the io.Writer decorator stack (gzip → bufio), applied to requests.
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.
structuralAdapterConvert the interface of a type into another interface clients expect, letting otherwise-incompatible types work together.
behavioralStrategyDefine a family of interchangeable algorithms, encapsulate each one, and select which to use at runtime.
behavioralChain of ResponsibilityPass a request along a chain of handlers until one of them handles it, decoupling sender from receiver.
Check your understanding
Score: 0 / 51. 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.
4. Which design principle does Decorator most embody?
Decorator lets the base type stay closed for modification while remaining open for extension: new responsibilities arrive as new wrapper types, so you never reopen DarkRoast to add Mocha.
5. In `h = Logging(AddHeader(handler))`, which middleware's code runs first on a request?
Each wrapper runs its 'before' code, calls next.ServeHTTP, then its 'after' code. Execution unwinds outermost-first on the way in and innermost-first on the way out — so wrapping order is the request pipeline, and getting it wrong (auth after logging body, say) is a real bug.
Comments
Sign in with GitHub to join the discussion.