# Decorator

> Wrap an object in another object with the same interface that adds behavior before or after delegating, so features stack without subclassing.

- kind: pattern
- category: structural
- aliases: Wrapper
- canonical: https://aipatterns.dev/design-patterns/decorator
- inspired-by: https://refactoring.guru/design-patterns/decorator

## Intent

Add responsibilities to an object at runtime by wrapping it in another object that implements the same interface. The wrapper does its extra work (retry, cache, log, measure, authorize) and delegates the core call to the object inside. Because the wrapper looks exactly like the original, wrappers nest in any order.

## Forces

- **Optional features** vs **subclass explosion**: retry, caching and logging in every combination would need `RetryingCachingLoggingRates` and seven siblings.
- **Cross-cutting concerns** vs **focused classes**: the HTTP client should fetch rates, not also count latency and back off on timeouts.
- **Configuring per use** vs **fixed at compile time**: tests want no retries, production wants three, a batch job wants caching too.
- **Ordering matters** vs **hidden ordering**: cache-outside-retry behaves differently from retry-outside-cache, and the order must be visible somewhere.

## Therefore

**Implement the component's interface in a wrapper class that holds another instance of that interface, add your behavior around the delegated call, and compose wrappers at the composition root.**

Each decorator does one thing. The client receives the outermost wrapper as a plain `RateSource` and cannot tell it from the real one. The stacking order is written once, where the object graph is built.

## Structure

```mermaid
classDiagram
  class RateSource {
    <<interface>>
    +rate(pair) Number
  }
  class RemoteRates {
    +rate(pair) Number
  }
  class RateDecorator {
    <<abstract>>
    -RateSource inner
    +rate(pair) Number
  }
  class Retrying {
    -int attempts
    +rate(pair) Number
  }
  class Caching {
    -Map cache
    +rate(pair) Number
  }
  class Logging {
    +rate(pair) Number
  }
  RateSource <|.. RemoteRates
  RateSource <|.. RateDecorator
  RateDecorator o-- RateSource : inner
  RateDecorator <|-- Retrying
  RateDecorator <|-- Caching
  RateDecorator <|-- Logging
```

### Participants

- **Component** (`RateSource`): the interface both the real object and every wrapper implement.
- **Concrete component** (`RemoteRates`): the object doing the core work.
- **Base decorator** (`RateDecorator`): optional; holds `inner` and forwards by default. Useful when the interface has many methods and each decorator overrides only a few. With one method, skip it, as the example does.
- **Concrete decorators** (`Retrying`, `Caching`, `Logging`): one added behavior each.
- **Client**: depends on `RateSource` only. The composition root decides the stack.

## Use it when

- You need to add retry, caching, metrics, tracing, rate limiting or authorization to an object without editing it.
- Features are optional and combinable, and each combination would otherwise be a subclass.
- You cannot subclass the component (it is `final`, sealed, or built by a third-party factory), but you can wrap it.
- Different environments need different stacks: no cache in tests, extra logging in staging.

## Do not use it when

- The interface has one method and function composition fits. `withRetry(withCache(fetchRate))` is the whole decorator stack, with no wrapper classes.
- You are in a web framework or RPC stack that already has middleware or interceptors (Express middleware, Go `http.Handler` wrappers, gRPC interceptors, Spring AOP). Use that mechanism instead of hand-rolled wrapper classes.
- The extra behavior is always on. Put it in the class itself; a wrapper that every instance always gets is just indirection.
- The interface is wide (twenty methods) and you only want to change one. Every decorator must forward the other nineteen; prefer a function hook, an event, or a narrower interface.
- Callers need to reach the concrete type behind the wrapper (`instanceof RemoteRates`). Decorators break identity checks; fix the caller first.
- The wrapper changes the interface. That is [Adapter](https://aipatterns.dev/design-patterns/adapter.md).

Note that Python's `@decorator` syntax is a related but different tool: it wraps a function at definition time. For one function, it is often the right choice over this pattern.

## Application grill

**Inspect first:** the cross-cutting behavior already present as middleware, wrappers or higher-order functions (fetch wrappers, tracing clients) - the pattern may exist unlabelled; where instances are constructed, since stacking needs a wiring point; the interface's stability.

**Missing pieces:** usually none for in-process stacking. If decorators would wrap an HTTP client for retries, caching or auth, check what the platform already provides (interceptors, SDK middleware stacks, OpenTelemetry instrumentation) before writing wrappers.

**Ask before you build:**

- **Scale:** how many behaviors stack on one object - three deep is readable, ten deep means the composition is the problem?
- **Performance:** what does each layer allocate per call on hot paths?
- **Reliability:** which layer owns errors, and is the ordering load-bearing (does auth wrap retry, or retry wrap auth)?
- **Budget:** who owns the wiring order - it is logic, not configuration, and reordering changes behavior?
- **Implementation:** do the same tests pass for base and decorated instances (the contract of an exact interface)?

## Implementation steps

1. Make sure callers depend on an interface, not on the concrete class. Extract one if needed ([Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md)).
2. Write one decorator per concern. Each takes the inner component in its constructor and implements the full interface.
3. In each method, do the extra work and delegate to `inner`. Forward unchanged methods as-is.
4. Remove the cross-cutting code from the concrete component once a decorator covers it.
5. Build the stack in one place (composition root, DI module, factory function) and document the order there.
6. Test each decorator alone against a fake inner component.

## Example

A pricing service reads currency rates from a flaky upstream API. Production wraps it with retry, a cache and logging; tests use the bare fake.

```ts title="rates.ts"
interface RateSource {
  rate(pair: string): number;
}

// Concrete component: an upstream API that times out on every other call.
class RemoteRates implements RateSource {
  private calls = 0;
  private readonly table: Record<string, number> = { "EUR/USD": 1.09, "GBP/USD": 1.27 };
  rate(pair: string): number {
    if (++this.calls % 2 === 1) throw new Error(`timeout fetching ${pair}`);
    console.log(`  [remote] served ${pair}`);
    return this.table[pair];
  }
}

class Retrying implements RateSource {
  constructor(private readonly inner: RateSource, private readonly attempts: number) {}
  rate(pair: string): number {
    for (let i = 1; ; i++) {
      try {
        return this.inner.rate(pair);
      } catch (e) {
        if (i >= this.attempts) throw e;
        console.log(`  [retry] attempt ${i} failed: ${(e as Error).message}`);
      }
    }
  }
}

class Caching implements RateSource {
  private readonly cache = new Map<string, number>();
  constructor(private readonly inner: RateSource) {}
  rate(pair: string): number {
    let r = this.cache.get(pair);
    if (r === undefined) {
      r = this.inner.rate(pair);
      this.cache.set(pair, r);
    }
    return r;
  }
}

class Logging implements RateSource {
  constructor(private readonly inner: RateSource) {}
  rate(pair: string): number {
    const r = this.inner.rate(pair);
    console.log(`rate(${pair}) = ${r}`);
    return r;
  }
}

// Order is explicit: cache hits skip retries; retries happen below the cache.
const rates: RateSource = new Logging(new Caching(new Retrying(new RemoteRates(), 3)));

const toUsd = (source: RateSource, amount: number, currency: string) =>
  amount * source.rate(`${currency}/USD`);

console.log(toUsd(rates, 100, "EUR").toFixed(2)); // timeout, retry, remote, 109.00
console.log(toUsd(rates, 250, "EUR").toFixed(2)); // cache hit, 272.50
console.log(toUsd(rates, 40, "GBP").toFixed(2));  // timeout, retry, remote, 50.80
```

```python title="rates.py"
from typing import Protocol

class RateSource(Protocol):
    def rate(self, pair: str) -> float: ...

class RemoteRates:
    """Upstream API that times out on every other call."""

    def __init__(self) -> None:
        self._calls = 0
        self._table = {"EUR/USD": 1.09, "GBP/USD": 1.27}

    def rate(self, pair: str) -> float:
        self._calls += 1
        if self._calls % 2 == 1:
            raise TimeoutError(f"timeout fetching {pair}")
        print(f"  [remote] served {pair}")
        return self._table[pair]

class Retrying:
    def __init__(self, inner: RateSource, attempts: int) -> None:
        self._inner, self._attempts = inner, attempts

    def rate(self, pair: str) -> float:
        for i in range(1, self._attempts + 1):
            try:
                return self._inner.rate(pair)
            except TimeoutError as e:
                if i == self._attempts:
                    raise
                print(f"  [retry] attempt {i} failed: {e}")
        raise AssertionError("unreachable")

class Caching:
    def __init__(self, inner: RateSource) -> None:
        self._inner = inner
        self._cache: dict[str, float] = {}

    def rate(self, pair: str) -> float:
        if pair not in self._cache:
            self._cache[pair] = self._inner.rate(pair)
        return self._cache[pair]

class Logging:
    def __init__(self, inner: RateSource) -> None:
        self._inner = inner

    def rate(self, pair: str) -> float:
        r = self._inner.rate(pair)
        print(f"rate({pair}) = {r}")
        return r

def to_usd(source: RateSource, amount: float, currency: str) -> float:
    return amount * source.rate(f"{currency}/USD")

if __name__ == "__main__":
    rates = Logging(Caching(Retrying(RemoteRates(), attempts=3)))
    print(f"{to_usd(rates, 100, 'EUR'):.2f}")  # timeout, retry, remote, 109.00
    print(f"{to_usd(rates, 250, 'EUR'):.2f}")  # cache hit, 272.50
    print(f"{to_usd(rates, 40, 'GBP'):.2f}")   # timeout, retry, remote, 50.80
```

```go title="rates.go"
package main

import (
	"fmt"
	"log"
)

type RateSource interface {
	Rate(pair string) (float64, error)
}

// RemoteRates times out on every other call.
type RemoteRates struct{ calls int }

func (r *RemoteRates) Rate(pair string) (float64, error) {
	r.calls++
	if r.calls%2 == 1 {
		return 0, fmt.Errorf("timeout fetching %s", pair)
	}
	fmt.Println("  [remote] served", pair)
	return map[string]float64{"EUR/USD": 1.09, "GBP/USD": 1.27}[pair], nil
}

type Retrying struct {
	Inner    RateSource
	Attempts int
}

func (r Retrying) Rate(pair string) (float64, error) {
	var err error
	for i := 1; i <= r.Attempts; i++ {
		var v float64
		if v, err = r.Inner.Rate(pair); err == nil {
			return v, nil
		}
		fmt.Printf("  [retry] attempt %d failed: %v\n", i, err)
	}
	return 0, err
}

type Caching struct {
	Inner RateSource
	cache map[string]float64
}

func NewCaching(inner RateSource) *Caching {
	return &Caching{Inner: inner, cache: map[string]float64{}}
}

func (c *Caching) Rate(pair string) (float64, error) {
	if v, ok := c.cache[pair]; ok {
		return v, nil
	}
	v, err := c.Inner.Rate(pair)
	if err == nil {
		c.cache[pair] = v
	}
	return v, err
}

type Logging struct{ Inner RateSource }

func (l Logging) Rate(pair string) (float64, error) {
	v, err := l.Inner.Rate(pair)
	fmt.Printf("rate(%s) = %v err=%v\n", pair, v, err)
	return v, err
}

func main() {
	var rates RateSource = Logging{NewCaching(Retrying{Inner: &RemoteRates{}, Attempts: 3})}
	for _, q := range []struct {
		amount   float64
		currency string
	}{{100, "EUR"}, {250, "EUR"}, {40, "GBP"}} {
		r, err := rates.Rate(q.currency + "/USD")
		if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("%.2f\n", q.amount*r)
	}
}
```

```java title="Rates.java"
import java.util.HashMap;
import java.util.Map;

public class Rates {
    interface RateSource {
        double rate(String pair);
    }

    /** Upstream API that times out on every other call. */
    static final class RemoteRates implements RateSource {
        private int calls = 0;
        private final Map<String, Double> table = Map.of("EUR/USD", 1.09, "GBP/USD", 1.27);

        public double rate(String pair) {
            if (++calls % 2 == 1) throw new IllegalStateException("timeout fetching " + pair);
            System.out.println("  [remote] served " + pair);
            return table.get(pair);
        }
    }

    record Retrying(RateSource inner, int attempts) implements RateSource {
        public double rate(String pair) {
            for (int i = 1; ; i++) {
                try {
                    return inner.rate(pair);
                } catch (IllegalStateException e) {
                    if (i >= attempts) throw e;
                    System.out.println("  [retry] attempt " + i + " failed: " + e.getMessage());
                }
            }
        }
    }

    static final class Caching implements RateSource {
        private final RateSource inner;
        private final Map<String, Double> cache = new HashMap<>();
        Caching(RateSource inner) { this.inner = inner; }

        public double rate(String pair) {
            Double hit = cache.get(pair);
            if (hit != null) return hit;
            double r = inner.rate(pair);
            cache.put(pair, r);
            return r;
        }
    }

    record Logging(RateSource inner) implements RateSource {
        public double rate(String pair) {
            double r = inner.rate(pair);
            System.out.println("rate(" + pair + ") = " + r);
            return r;
        }
    }

    static double toUsd(RateSource source, double amount, String currency) {
        return amount * source.rate(currency + "/USD");
    }

    public static void main(String[] args) {
        RateSource rates = new Logging(new Caching(new Retrying(new RemoteRates(), 3)));
        System.out.printf("%.2f%n", toUsd(rates, 100, "EUR")); // timeout, retry, remote, 109.00
        System.out.printf("%.2f%n", toUsd(rates, 250, "EUR")); // cache hit, 272.50
        System.out.printf("%.2f%n", toUsd(rates, 40, "GBP"));  // timeout, retry, remote, 50.80
    }
}
```

The function-composition version of the same stack in TypeScript is `const rate = logged(cached(retried(fetchRate, 3)))`, where each helper takes and returns `(pair: string) => number`. Prefer it when the interface is a single function.

## Trade-offs

**Gains**
- Each concern is a small class with a single job, tested alone.
- Features combine freely at runtime without a subclass per combination.
- The core component stays free of retry, cache and logging code.
- You can decorate classes you cannot modify or subclass.

**Costs**
- Behavior depends on stacking order, and the wrong order (retry outside cache, logging inside retry) produces subtle bugs.
- Stack traces and debuggers show layers of wrappers.
- Identity and type checks fail: the outer object is not a `RemoteRates`.
- Wide interfaces force each decorator to forward every method, which invites copy-paste.

## Relations

- [Adapter](https://aipatterns.dev/design-patterns/adapter.md) changes an object's interface. Decorator keeps it and adds behavior.
- [Proxy](https://aipatterns.dev/design-patterns/proxy.md) has the same shape, but controls access to the subject (lazy creation, permission, remote calls) and usually manages its lifecycle. Decorators are stacked by the client; a proxy usually is not.
- [Composite](https://aipatterns.dev/design-patterns/composite.md) also recurses through a shared interface, but aggregates many children. A decorator has exactly one.
- [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) links handlers in the same way, but any handler may stop the request. Decorators normally always delegate.
- [Strategy](https://aipatterns.dev/design-patterns/strategy.md) swaps an object's internals. Decorator changes its outside.

## Agent checklist

- [ ] Callers depend on an interface, so a wrapper can be substituted without changing them.
- [ ] You considered function composition or the framework's middleware before writing wrapper classes.
- [ ] Each decorator does one thing and forwards every method it does not change.
- [ ] The stacking order is defined in one place and its reasoning is in a comment.
- [ ] No caller checks the concrete type of the component after the change.
- [ ] Each decorator has a test against a fake inner component, including the error path.
