The Decorator Pattern allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. It is the architectural foundation for Middleware, Interceptors, and Resilient Clients.
In a decorator stack, the order of wrapping determines the execution flow.
Logging(Retry(Caching(Service))) vs Caching(Logging(Retry(Service))).
By composing simple decorators, we can build a production-grade HTTP client with multiple cross-cutting concerns.
interface HttpClient { String get(String url); }
class BaseClient implements HttpClient {
@Override public String get(String url) { /* actual I/O */ return ""; }
}
class LoggingDecorator implements HttpClient {
private final HttpClient inner;
public LoggingDecorator(HttpClient inner) { this.inner = inner; }
@Override public String get(String url) {
System.out.println("Calling: " + url);
return inner.get(url);
}
}
class RetryDecorator implements HttpClient {
private final HttpClient inner;
@Override public String get(String url) {
return retry(() -> inner.get(url)); // Logic to retry on 5xx
}
}
// Composition: Resilient Client
HttpClient client = new LoggingDecorator(new RetryDecorator(new BaseClient()));
A perfect decorator is Transparent: the client should not know it is talking to a wrapper.
instanceof or reflection to check for a specific implementation, decorators will break it.For cross-cutting concerns that apply to many interfaces (e.g., generic metrics), use java.lang.reflect.Proxy. This avoids creating a manual decorator class for every interface in your system.
HttpClient proxy = (HttpClient) Proxy.newProxyInstance(
HttpClient.class.getClassLoader(),
new Class<?>[]{HttpClient.class},
(p, method, args) -> {
// Intercept logic here
return method.invoke(realObject, args);
}
);
See Also: