Appearance
Decorator Pattern
GoF Patterns Series — Structural · See also: Adapter · Facade
Introduction
Cross-cutting concerns — logging, caching, metrics, authorization — need to be applied to many services. The instinctive response is to reach for inheritance: create LoggedUserService, CachedUserService, and then LoggedCachedUserService when both are needed. That instinct is wrong. The combination explosion grows factorially with every new concern. The Decorator pattern solves this by wrapping an object in a series of transparent shells, each adding exactly one behavior, composable in any order at runtime without modifying any underlying class.
Intent
Attach additional responsibilities to an object dynamically by wrapping it in decorator objects that implement the same interface.
Structure
Call Chain at Runtime
Participants
| Role | Responsibility |
|---|---|
Component (UserService) | The shared interface. Both the concrete component and all decorators implement it. |
ConcreteComponent (BasicUserService) | The core implementation that performs real work. |
Decorator (UserServiceDecorator) | Abstract base that holds a reference to a UserService and delegates all calls. Optional, but reduces boilerplate in concrete decorators. |
ConcreteDecorators (LoggingUserServiceDecorator, etc.) | Each adds exactly one cross-cutting concern, before or after delegating. |
Anti-Pattern — Subclass Explosion
Inheritance ties specific combinations into hard-wired class names. The more concerns you add, the worse it gets.
java
// ANTI-PATTERN: one subclass per feature combination
public class UserService { /* core logic */ }
// One concern: logging
public class LoggedUserService extends UserService {
@Override public User findById(String id) {
log.info("findById called: {}", id);
User u = super.findById(id);
log.info("findById returned: {}", u);
return u;
}
}
// One concern: caching
public class CachedUserService extends UserService {
@Override public User findById(String id) {
return cache.computeIfAbsent(id, super::findById);
}
}
// Two concerns — must create a dedicated class
public class LoggedCachedUserService extends CachedUserService {
@Override public User findById(String id) {
log.info("findById called: {}", id);
User u = super.findById(id); // cache already in play via CachedUserService
log.info("findById returned: {}", u);
return u;
}
// What if logging must wrap the outer layer but metrics wrap the inner?
// You need yet another class for that ordering.
}
// Three concerns — N concerns produce 2^N combinations
public class LoggedCachedMeteredUserService extends LoggedCachedUserService { /* ... */ }
public class CachedMeteredUserService extends CachedUserService { /* ... */ }
// ...and so onProblems with this design:
- With three independent concerns (logging, caching, metrics) applied in any order, you need up to eight subclasses just for
UserService. Each additional service class multiplies the explosion. - The ordering of concerns is fixed in the class hierarchy. Placing metrics outside logging requires a different class from placing it inside.
- Adding a fourth concern (say, authorization) requires revisiting and extending every existing combination.
- Subclasses are coupled to
UserService's concrete implementation; anyprotectedmethod or field change cascades through the hierarchy.
Correct Implementation
Step 1 — Define the Component interface.
java
public interface UserService {
User findById(String id);
User save(User user);
void delete(String id);
}Step 2 — Implement the ConcreteComponent with pure business logic.
java
@Repository
public class BasicUserService implements UserService {
private final UserRepository repo;
public BasicUserService(UserRepository repo) {
this.repo = repo;
}
@Override
public User findById(String id) {
return repo.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
@Override
public User save(User user) {
return repo.save(user);
}
@Override
public void delete(String id) {
repo.deleteById(id);
}
}Step 3 — Create the abstract Decorator base to eliminate forwarding boilerplate.
java
public abstract class UserServiceDecorator implements UserService {
protected final UserService delegate;
protected UserServiceDecorator(UserService delegate) {
this.delegate = delegate;
}
// Default forwarding — subclasses override only the methods they need
@Override
public User findById(String id) { return delegate.findById(id); }
@Override
public User save(User user) { return delegate.save(user); }
@Override
public void delete(String id) { delegate.delete(id); }
}Step 4 — Each concrete decorator adds exactly one concern.
java
public class LoggingUserServiceDecorator extends UserServiceDecorator {
private static final Logger log = LoggerFactory.getLogger(LoggingUserServiceDecorator.class);
public LoggingUserServiceDecorator(UserService delegate) {
super(delegate);
}
@Override
public User findById(String id) {
log.debug("findById: id={}", id);
long start = System.currentTimeMillis();
try {
User result = delegate.findById(id);
log.debug("findById: id={} returned userId={} in {}ms",
id, result.getId(), System.currentTimeMillis() - start);
return result;
} catch (RuntimeException ex) {
log.error("findById: id={} threw {}", id, ex.getMessage());
throw ex;
}
}
@Override
public User save(User user) {
log.info("save: userId={}", user.getId());
User saved = delegate.save(user);
log.info("save: persisted userId={}", saved.getId());
return saved;
}
}java
public class CachingUserServiceDecorator extends UserServiceDecorator {
private final Cache<String, User> cache;
public CachingUserServiceDecorator(UserService delegate, Cache<String, User> cache) {
super(delegate);
this.cache = cache;
}
@Override
public User findById(String id) {
User cached = cache.getIfPresent(id);
if (cached != null) {
return cached;
}
User user = delegate.findById(id);
cache.put(id, user);
return user;
}
@Override
public User save(User user) {
User saved = delegate.save(user);
// Invalidate stale entry; the next findById will re-populate
cache.invalidate(saved.getId());
return saved;
}
@Override
public void delete(String id) {
delegate.delete(id);
cache.invalidate(id);
}
}java
public class MetricsUserServiceDecorator extends UserServiceDecorator {
private final MeterRegistry registry;
public MetricsUserServiceDecorator(UserService delegate, MeterRegistry registry) {
super(delegate);
this.registry = registry;
}
@Override
public User findById(String id) {
return Timer.builder("user.service.findById")
.tag("operation", "findById")
.register(registry)
.recordCallable(() -> delegate.findById(id));
}
@Override
public User save(User user) {
registry.counter("user.service.save").increment();
return delegate.save(user);
}
@Override
public void delete(String id) {
registry.counter("user.service.delete").increment();
delegate.delete(id);
}
}Step 5 — Compose the stack in the configuration layer; the order is explicit and changeable.
java
@Configuration
public class UserServiceConfig {
@Bean
public UserService userService(UserRepository repo,
Cache<String, User> userCache,
MeterRegistry meterRegistry) {
UserService core = new BasicUserService(repo);
// Stack order: Metrics → Logging → Caching → BasicUserService
// Each wrapper sees the result of everything inside it.
// Changing the order here changes behavior without touching any decorator class.
return new MetricsUserServiceDecorator(
new LoggingUserServiceDecorator(
new CachingUserServiceDecorator(core, userCache)),
meterRegistry);
}
}Any client injecting UserService receives the fully decorated stack with no knowledge of what it contains.
Behavior Is Composable and Stackable at Runtime
Unlike inheritance, the decorator stack is constructed at runtime. The same LoggingUserServiceDecorator class can wrap BasicUserService in one environment and wrap a CachingUserServiceDecorator in another. Feature flags can swap the stack in response to configuration:
java
UserService service = new BasicUserService(repo);
if (config.isCachingEnabled()) {
service = new CachingUserServiceDecorator(service, cache);
}
if (config.isLoggingEnabled()) {
service = new LoggingUserServiceDecorator(service);
}
if (config.isMetricsEnabled()) {
service = new MetricsUserServiceDecorator(service, registry);
}This is impossible with inheritance — you cannot enable or disable a class from the hierarchy at runtime.
Real-World Examples
| Example | Inner Component | Decorator |
|---|---|---|
new BufferedReader(new FileReader(path)) | FileReader (raw chars from disk) | BufferedReader (adds buffering) |
new BufferedReader(new InputStreamReader(socket.getInputStream())) | InputStream chain | Reader with buffering |
Spring TransactionAwareDataSourceProxy | DataSource | Wraps with transaction-awareness |
| Spring Security filter chain | HttpServlet handler | Each Filter decorates the chain |
AWS SDK SdkHttpClient interceptors | HTTP transport | Request/response interceptors applied in sequence |
| SLF4J MDC-aware logging wrappers | Logger | Adds diagnostic context before delegating |
Java I/O is the canonical example: BufferedInputStream wraps FileInputStream, which wraps a file descriptor. Each wraps the same InputStream interface and adds one behavior (buffering, checksumming, decompression, decryption) without touching the classes inside.
Open/Closed Principle
The Decorator pattern is a direct embodiment of the Open/Closed Principle: the BasicUserService is closed for modification, yet the system is open for extension. Adding audit logging means writing one new decorator class and adjusting the wiring, with zero changes to any existing class. The component interface is the stable abstraction that all decorators and the core implementation share.
When to Use
- You need to add responsibilities to individual objects, not to an entire class.
- The number of combinations of behaviors would produce an unmanageable class hierarchy with inheritance.
- You need to add or remove responsibilities at runtime based on configuration or feature flags.
- Cross-cutting concerns (logging, caching, metrics, authorization) must be separated from business logic without modifying the business class.
Consequences
Benefits
- Eliminates class explosion. Three independent decorators replace the eight subclasses you would need with inheritance.
- Single Responsibility. Each decorator addresses one concern. The core implementation addresses only business logic.
- Runtime composition. The stack can be built from configuration or feature flags without recompiling.
- OCP-compliant. Adding a new concern adds one new class; nothing existing is modified.
Trade-offs
- Identity and equality. A decorated object is not
instanceofBasicUserService. Identity-based logic (==,instanceof) fails unless it checks the interface rather than the class. - Debugging complexity. Stack traces pass through multiple decorator frames before reaching real logic. Structured logging in each decorator mitigates this.
- Order sensitivity. Placing the metrics decorator inside the logging decorator means metrics do not capture logging overhead, and vice versa. The correct order must be understood and documented.
- Interface changes are costly. Adding a method to
UserServicerequires updating the abstract base decorator and potentially all concrete decorators, even those that do not care about the new method.
Related Concepts
- Adapter — also wraps an object, but translates between different interfaces rather than adding behavior to the same interface.
- Facade — simplifies a complex subsystem behind one interface; does not add behavior through wrapping.
- Open/Closed Principle — Decorator is a primary mechanism for achieving OCP: extend through new decorator classes, never by modifying existing ones.
- Single Responsibility Principle — each decorator has exactly one reason to change: the cross-cutting concern it owns.
- Dependency Inversion Principle — every decorator and the core component depend on the
UserServiceabstraction, never on concrete implementations. - Hexagonal Architecture — decorators are a natural fit for driven-port adapters that add infrastructure concerns (metrics, caching) around the application core without contaminating domain logic.