Design patterns: Adapter, Decorator, Proxy, Facade and Composite
Structural patterns combine objects into larger structures while keeping them flexible:
- Adapter: makes an existing class fit the interface your code expects. InputStreamReader adapts bytes to characters; Arrays.asList adapts an array to a List.
- Decorator: wraps an object of the same interface to add behaviour: buffering, logging, caching. new BufferedReader(new FileReader(...)) is a decorator chain.
- Proxy: stands in for an object to control access: lazy loading (Hibernate's lazy relations), security checks, transactions. Spring's @Transactional works through a proxy.
- Facade: one simple entry point in front of a complicated subsystem. An OrderFacade.placeOrder() that coordinates inventory, payment and email; SLF4J is a facade over logging libraries.
- Composite: treat a single object and a group of objects the same way: a folder and a file both have size(), and a folder's size is the sum of its children.
All of them rely on composition and interfaces, which is why they show up everywhere in frameworks.
Example
interface Notifier { void send(String to, String message); }
class EmailNotifier implements Notifier {
public void send(String to, String message) { System.out.println("Email to " + to + ": " + message); }
}
// Decorator: same interface, wraps another Notifier, adds one behaviour
class RetryingNotifier implements Notifier {
private final Notifier inner;
RetryingNotifier(Notifier inner) { this.inner = inner; }
public void send(String to, String message) {
for (int attempt = 1; ; attempt++) {
try { inner.send(to, message); return; }
catch (RuntimeException e) { if (attempt == 3) throw e; }
}
}
}
// Adapter: makes a third-party SMS client look like a Notifier
class SmsAdapter implements Notifier {
private final ThirdPartySmsClient client;
SmsAdapter(ThirdPartySmsClient client) { this.client = client; }
public void send(String to, String message) { client.dispatchText(new SmsRequest(to, message)); }
}
Notifier notifier = new RetryingNotifier(new EmailNotifier()); // stack decorators freely
notifier.send("asha@example.com", "Your course is ready");sealed interface Node permits FileNode, Folder { long size(); }
record FileNode(String name, long size) implements Node {}
record Folder(String name, List<Node> children) implements Node {
public long size() { return children.stream().mapToLong(Node::size).sum(); } // recursion
}
Node project = new Folder("src", List.of(new FileNode("App.java", 1200),
new Folder("model", List.of(new FileNode("User.java", 800)))));
System.out.println(project.size()); // 2000Common mistake
Calling a @Transactional (or @Cacheable, @Async) method from another method of the same bean and expecting the annotation to work. The internal call bypasses the proxy.
Under the hood
Decorator and Proxy look identical in code (both wrap an object with the same interface); the difference is intent. A decorator adds features the caller asks for; a proxy controls access, often invisibly, as Spring's proxies do. That invisibility is also why calling a @Transactional method from inside the same class skips the transaction: the call never goes through the proxy.
Check yourself
new BufferedReader(new FileReader("a.txt")) is an example of which pattern?
How this connects
Know these first
Where this leads
You've reached the end of this thread. Try a learning path for what's next.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.