Table of contents
- What It Means to Wrap
- Decorator — Wrapping to Add Features
- Proxy — Wrapping to Control Access
- Decorator vs Proxy — Same Structure, Different Intent
- Decision Criteria in Practice
What It Means to Wrap
When you want to add functionality to an object, the first approach that comes to mind is inheritance. Something like LoggingUserService extends UserService to extend the existing class. Simple and intuitive.
But problems arise as you add features through inheritance. A service that needs logging, a service that needs caching, a service that needs both — each combination requires a subclass. Three features produce up to seven combinations. Four features produce fifteen. This is the class explosion problem. As inheritance hierarchies deepen, anyone reading the code has to jump between multiple classes tracing “so what does this method actually do?”
Decorator and Proxy offer the same structural solution to this problem. Create a new object that wraps the original, but implement the same interface as the original. From the outside, the original and the wrapper look identical. The difference lies in their intent. Decorator aims to add functionality, while Proxy aims to control access.
Decorator — Wrapping to Add Features
Structure of the Pattern
The Decorator pattern wraps an object with another object, adding new behavior on top of the original’s behavior. Because the wrapping object (Decorator) and the wrapped object (Component) implement the same interface, the client cannot tell them apart.
classDiagram
class Component {
<<interface>>
+operation()
}
class ConcreteComponent {
+operation()
}
class Decorator {
<<abstract>>
-component: Component
+operation()
}
class ConcreteDecoratorA {
+operation()
}
class ConcreteDecoratorB {
+operation()
}
Component <|.. ConcreteComponent
Component <|.. Decorator
Decorator <|-- ConcreteDecoratorA
Decorator <|-- ConcreteDecoratorB
Decorator o-- Component : wraps
The key is that Decorator holds a Component as a field while also implementing Component itself. This structure allows decorators to be stacked in multiple layers.
Code Example — Notification System
A scenario where the base notification is email, with SMS or Slack notifications added on as needed.
Define the interface.
public interface Notifier {
void send(String message);
}
The base implementation is email notification.
public class EmailNotifier implements Notifier {
private final String email;
public EmailNotifier(String email) {
this.email = email;
}
@Override
public void send(String message) {
System.out.println("Email sent -> " + email + ": " + message);
}
}
Create the decorator base class. This class establishes the wrapping structure.
public abstract class NotifierDecorator implements Notifier {
protected final Notifier wrapped;
protected NotifierDecorator(Notifier wrapped) {
this.wrapped = wrapped;
}
@Override
public void send(String message) {
wrapped.send(message); // Delegate to the original
}
}
What this class does is wrap a Notifier and forward send() calls to the original as-is. Nothing is added yet.
Implement the SMS and Slack decorators.
public class SmsDecorator extends NotifierDecorator {
private final String phoneNumber;
public SmsDecorator(Notifier wrapped, String phoneNumber) {
super(wrapped);
this.phoneNumber = phoneNumber;
}
@Override
public void send(String message) {
super.send(message); // Execute existing notification
System.out.println("SMS sent -> " + phoneNumber + ": " + message);
}
}
public class SlackDecorator extends NotifierDecorator {
private final String channel;
public SlackDecorator(Notifier wrapped, String channel) {
super(wrapped);
this.channel = channel;
}
@Override
public void send(String message) {
super.send(message); // Execute existing notification
System.out.println("Slack sent -> #" + channel + ": " + message);
}
}
Stacking decorators looks like this.
Notifier notifier = new EmailNotifier("dev@example.com");
notifier = new SmsDecorator(notifier, "010-1234-5678");
notifier = new SlackDecorator(notifier, "alerts");
notifier.send("Server outage detected");
Running this code sends email, SMS, and Slack notifications in order. Each decorator sends its own notification and delegates to the inner decorator (or original) via super.send(). No need for multiple inheritance hierarchies — functionality is extended through composition alone.
Java I/O — The Textbook Decorator Example
Java’s java.io package is the most famous real-world application of the Decorator pattern.
// Base stream that reads bytes from a file
InputStream raw = new FileInputStream("data.txt");
// Add buffering
InputStream buffered = new BufferedInputStream(raw);
// Add GZIP decompression
InputStream decompressed = new GZIPInputStream(buffered);
FileInputStream, BufferedInputStream, and GZIPInputStream all implement InputStream. BufferedInputStream wraps FileInputStream to add buffering, and GZIPInputStream wraps BufferedInputStream to add decompression. From the outside, they are all just InputStream.
Why is this structure powerful? Because each feature is an independent decorator, you can freely mix and match combinations. You can use just buffering, just GZIP, or both. Adding new stream features (encryption, Base64 decoding, etc.) does not require modifying existing classes.
The downsides are also clear. When you first see Java I/O code like new BufferedReader(new InputStreamReader(new FileInputStream("data.txt"), "UTF-8")), the nesting can be dizzying. The wrapping order matters, but the compiler does not enforce it, so wrapping in the wrong order only reveals problems at runtime.
Decorator in Kotlin
Using Kotlin’s by keyword (delegation) reduces decorator boilerplate.
interface Notifier {
fun send(message: String)
}
class EmailNotifier(private val email: String) : Notifier {
override fun send(message: String) {
println("Email sent -> $email: $message")
}
}
class SmsDecorator(
private val wrapped: Notifier,
private val phone: String
) : Notifier by wrapped { // Delegate to wrapped
override fun send(message: String) {
wrapped.send(message)
println("SMS sent -> $phone: $message")
}
}
by wrapped automatically delegates all methods of Notifier to wrapped. You only need to implement the methods you want to override. Even if the interface has ten methods, you only touch the one you care about. There is no need for a separate base decorator class.
Proxy — Wrapping to Control Access
Structure of the Pattern
The Proxy pattern also wraps the original object. Its class diagram is nearly identical to Decorator’s. But the purpose is different. Proxy controls access to the original. Rather than adding features, the key is checking or optimizing something before reaching the original.
Three commonly used types of Proxy:
- Protection Proxy: Checks access permissions. Blocks the call to the original if permissions are insufficient.
- Virtual Proxy: Defers creation of a heavy object until it is actually needed. Lazy loading.
- Caching Proxy: Caches the original’s results, returning cached values for identical requests without calling the original.
Code Example — Lazy Loading Proxy
Database connections are heavy resources. Instead of connecting immediately at application startup, let us build a lazy loading proxy that connects only when the first query arrives.
sequenceDiagram
participant Client
participant Proxy as DatabaseProxy
participant Real as RealDatabase
Client->>Proxy: query("SELECT ...")
alt Not yet connected
Proxy->>Real: connect()
Real-->>Proxy: Connection established
end
Proxy->>Real: query("SELECT ...")
Real-->>Proxy: Results
Proxy-->>Client: Results
Note over Proxy: From the second query on,<br/>connect() is skipped
Define the interface.
public interface Database {
List<Map<String, Object>> query(String sql);
void close();
}
The real implementation connects to the DB upon creation.
public class RealDatabase implements Database {
private final Connection connection;
public RealDatabase(String url, String user, String password) {
// The actual DB connection is made at this point (heavy operation)
this.connection = DriverManager.getConnection(url, user, password);
System.out.println("DB connection established: " + url);
}
@Override
public List<Map<String, Object>> query(String sql) {
// Actual query execution logic
// ...
return results;
}
@Override
public void close() {
connection.close();
}
}
The proxy defers RealDatabase creation until first use.
public class LazyDatabaseProxy implements Database {
private final String url;
private final String user;
private final String password;
private RealDatabase realDatabase; // Starts as null
public LazyDatabaseProxy(String url, String user, String password) {
this.url = url;
this.user = user;
this.password = password;
// No connection is made here!
}
private RealDatabase getRealDatabase() {
if (realDatabase == null) {
realDatabase = new RealDatabase(url, user, password);
}
return realDatabase;
}
@Override
public List<Map<String, Object>> query(String sql) {
return getRealDatabase().query(sql); // Connects on first call
}
@Override
public void close() {
if (realDatabase != null) {
realDatabase.close();
}
}
}
What this proxy does is hide RealDatabase inside itself and create the real object only when query() is first called. From the second call onward, it reuses the already-created object.
The client cannot distinguish the proxy from the real object.
Database db = new LazyDatabaseProxy("jdbc:mysql://localhost/mydb", "root", "pass");
// No DB connection exists at this point
// ...application initialization...
List<Map<String, Object>> users = db.query("SELECT * FROM users");
// The DB connection is established at this point
Caching Proxy Example
In situations where the same query is executed repeatedly, inserting a caching proxy reduces DB load.
public class CachingDatabaseProxy implements Database {
private final Database wrapped;
private final Map<String, List<Map<String, Object>>> cache = new HashMap<>();
public CachingDatabaseProxy(Database wrapped) {
this.wrapped = wrapped;
}
@Override
public List<Map<String, Object>> query(String sql) {
if (cache.containsKey(sql)) {
System.out.println("Cache hit: " + sql);
return cache.get(sql);
}
List<Map<String, Object>> result = wrapped.query(sql);
cache.put(sql, result);
return result;
}
@Override
public void close() {
cache.clear();
wrapped.close();
}
}
What this proxy does is use the SQL string as a key to cache results. When the same query arrives, it returns the cached result without hitting the DB. In production you would add TTL (Time-To-Live) or cache size limits, but this is the pattern’s core structure.
Combining the caching proxy and lazy loading proxy is also possible.
Database db = new CachingDatabaseProxy(
new LazyDatabaseProxy("jdbc:mysql://localhost/mydb", "root", "pass")
);
From the outside, it is just a Database. Internally, it checks the cache and, on a cache miss, goes through the lazy loading proxy to access the real DB.
Spring AOP and Proxy
If you are using the Spring Framework, you are already under the influence of the Proxy pattern. Spring’s AOP (Aspect-Oriented Programming) is implemented using Dynamic Proxies.
Taking @Transactional as an example, Spring creates a proxy object for that bean and registers it in the container. When a client calls a method, the proxy — not the real object — receives the call first. The proxy starts a transaction, invokes the actual method, commits if no exception occurs, and rolls back on exception.
@Service
public class OrderService {
@Transactional
public void placeOrder(OrderRequest request) {
// This method is not called directly.
// A proxy created by Spring opens a transaction,
// calls this method,
// and commits or rolls back based on the result.
orderRepository.save(request.toOrder());
paymentService.charge(request.getPaymentInfo());
}
}
Spring uses two proxy mechanisms.
- JDK Dynamic Proxy: When an interface exists. Uses
java.lang.reflect.Proxyto create the proxy object at runtime. - CGLIB Proxy: When no interface exists. Manipulates bytecode to create a subclass of the target class at runtime.
Either way, the client has no idea whether it is a proxy or the real object. This is the essence of the Proxy pattern — transparently interposing.
Not understanding this structure leads to a common Spring trap. Calling a @Transactional method from within the same class does not apply the transaction. this.placeOrder() calls the real object directly, bypassing the proxy. Understanding the Proxy pattern’s structure makes the cause of such issues immediately obvious.
Decorator vs Proxy — Same Structure, Different Intent
Looking only at the class diagram, Decorator and Proxy are indistinguishable. Both implement the same interface as the original and hold the original object as a field. The difference lies in why they wrap.
| Criterion | Decorator | Proxy |
|---|---|---|
| Purpose | Add functionality | Control access |
| Relationship with original | Stacks new behavior on top of the original | Intercepts before reaching the original |
| Layering | Core usage pattern (multiple layers) | Usually a single layer |
| Original object creation | Client creates the original and passes it in | Proxy can manage original creation |
| Typical examples | Java I/O, notification decorators | Spring AOP, lazy loading, caching |
| Intent question | ”I need to add features to this object" | "I need to control access to this object” |
Sometimes the boundary gets blurry. Caching can look like adding functionality or controlling access. Logging is the same way. In such cases, judge by “what is the primary purpose of this wrapping?” If caching’s primary purpose is “reducing calls to the original,” it is closer to Proxy. If it is “adding cache headers to the response,” it is closer to Decorator.
Decision Criteria in Practice
When torn between Decorator and Proxy, ask this:
“Is the reason for wrapping ‘adding’ or ‘blocking/deferring’?”
If adding, it is Decorator. Adding SMS to email notification, then adding Slack on top. Features accumulate. The more you wrap, the more the object can do.
If blocking or deferring, it is Proxy. Blocking unauthorized users, deferring creation of a heavy object, returning a cache for repeated requests. The key is making a decision before reaching the original.
Both patterns stand on the principle of “wrapping with the same interface.” Once you understand this principle, clarifying the intent of the wrapping naturally determines which pattern to use. The simpler the structure of a pattern, the more important it is to precisely name the intent.
In the next part, we explore three patterns that match interfaces, hide complexity, and treat parts and wholes uniformly. Adapter transforms an existing interface to the desired shape, Facade places a simple window in front of a complex subsystem, and Composite recursively handles tree structures.




Loading comments...