The idea: one interface, many wrappers

The client holds a UserRepository and calls repo.get(42). It does not know what object is behind that interface. It could be the real UserService, which queries Postgres. It could also be a wrapper: an object that implements the same interface, holds a reference to another UserRepository, and passes the call on. Wrappers can wrap wrappers, so you get a chain.

Four nested boxes, LoggingDecorator around AuthProxy around RetryDecorator around CachingProxy, around the real UserService. A blue arrow labelled get(42) goes inward through every box to UserService; a green arrow labelled User(42, Ann) comes back outward through the same boxes
The call goes in through every wrapper to the real object; the result comes back out through the same wrappers in reverse order.

On the canvas the chain is new LoggingDecorator(new AuthProxy(new RetryDecorator(new CachingProxy(UserService)))). The call travels inward on the upper lane, the result travels outward on the lower lane. The client code is the same with no wrappers (Demo 1: plain call, no wrappers), with one (Demo 2) or with four.

Decorator: add behaviour around the call

A decorator adds something before and after the call, and always passes the call on. LoggingDecorator writes a log line on the way in and another on the way out (Demo 2: add the logging decorator). RetryDecorator catches an exception on the way out and calls inward again, up to maxAttempts = 3 (Demo 4: retry on failure). The client never sees the first timeout.

Each decorator does one job, and you combine them by stacking. That is the alternative to subclasses like LoggingRetryingUserService for every mix. Java's I/O streams are the classic example: new BufferedReader(new InputStreamReader(new FileInputStream(f))).

Proxy: control access to the real object

A proxy stands in for the real object and decides whether, when and how the call reaches it. It may answer by itself. The page shows three kinds:

  • Protection proxy: AuthProxy checks the caller. A guest gets AccessDeniedException and the call never goes further in (Demo 5: access denied).
  • Caching proxy: CachingProxy returns a stored result on a hit. The real object and Postgres are never called (Demo 3: cache hit, the proxy answers).
  • Virtual proxy: UserService is expensive to build (it opens a database pool), so CachingProxy creates it only on the first miss. This is lazy initialization. In Demo 5 it is never created at all.
Three columns. Access denied: the call stops at AuthProxy with a red cross, UserService is not reached. Cache hit: the call stops at CachingProxy, which returns User(42, Ann) from its cache; Postgres gets no query. Lazy init: on the first miss CachingProxy creates UserService, which then queries Postgres
A proxy can refuse the call, answer it from a cache, or create the real object only when it is first needed.

A remote proxy is a fourth kind: a local stub with the same interface that sends the call over the network (gRPC and Java RMI stubs work this way).

Adapter: same shape, another interface

An adapter is built the same way: it holds a reference to another object and passes each call on. But the wrapped object, the adaptee, does not have the client's interface. Here LegacyUserApi is an old class with Map fetchUser(String userId). LegacyUserAdapter implements UserRepository turns get(42) into fetchUser("42"), the returned {user_name: "Ann"} into User(42, Ann), and the legacy LegacyApiError into the TimeoutException callers expect (Demo 6: adapter, same shape, other interface; pick the adapter wrappers and call get(7) for the error).

class LegacyUserAdapter implements UserRepository {
    private final LegacyUserApi api;              // the adaptee
    public User get(int id) {
        Map<String, String> m = api.fetchUser(String.valueOf(id));
        return new User(id, m.get("user_name"));
    }
}

It adds nothing and guards nothing: no log, no retry, no cache, and a guest gets the data, because there is no AuthProxy. Adapters let you plug in a third-party library, an old module or a remote API without changing the client; java.io.InputStreamReader adapts a byte stream to a Reader.

Decorator vs proxy vs adapter

In code they look the same: a class that implements the client's interface, a reference to the wrapped object, a method that calls it. The difference is the intent.

DecoratorProxyAdapter
Purposeadds behaviourcontrols accessconverts one interface into another
Wrapped object's interfacethe samethe samea different one
Calls the wrapped object?always (maybe more than once, like retry)only when it decides to (may deny or answer from a cache)always, once, with translated arguments
Who creates the wrapped object?usually the client passes it inoften the proxy itself (lazy, remote)passed in (an existing class)
Stackingmeant to be stacked in any combinationusually one per real objectone, at the boundary to the other code
On the canvasLoggingDecorator, RetryDecoratorAuthProxy, CachingProxyLegacyUserAdapter

The order of the wrappers matters

Each wrapper only sees what the wrappers outside it let through. AuthProxy sits outside the cache, so a guest is refused before any cached data is read. RetryDecorator sits outside the cache, so each attempt checks the cache again. LoggingDecorator is outermost, so it logs every call, including denied ones. Swap two wrappers and the behaviour changes, even though every class is the same. Spring's AOP proxies and Resilience4j's decorators have an explicit order for this reason.

See also Circuit Breaker and Bulkhead, where retry, circuit breaker and bulkhead are decorators around one call, and Cache Strategies, where the cache in front of the database is a caching proxy.

Wrapping is composition; see Inheritance vs Composition for the subclassing alternative and its class explosion.

What the page leaves out

Backoff between retries and which exceptions are worth retrying; cache expiry (TTL) and invalidation; thread safety of the lazy creation; dynamic proxies that are generated at run time (java.lang.reflect.Proxy, Spring AOP, Python decorators as functions); remote proxies and their network failures; the cost of each extra layer in a deep chain; and two-way or class (inheritance-based) adapters, and the Facade pattern, which gives a simpler interface to a whole subsystem rather than one object.