Problem solving and design ยท 2. Design and clean code, lesson 4 of 5

Design patterns: Adapter, Decorator, Proxy, Facade and Composite

Intermediate3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

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.

Diagram

Example

Java
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");
Composite: one interface for leaves and groups
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());   // 2000

Common 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

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.