Explore five essential structural design patterns in Java, including Adapter, Decorator, Facade, Proxy, and Composite, with practical examples and use cases.
Updated August 2026 — Structural Patterns (Part 2) restored for this URL.
TL;DR
-
Structural patterns focus on how classes and objects are composed to build larger, flexible structures.
-
Adapter converts one interface into another so incompatible classes can work together.
-
Decorator adds optional behavior to objects without modifying their underlying classes.
-
Facade and Proxy simplify access to complex systems or control how another object is accessed.
-
Composite lets you treat individual objects and groups of objects uniformly, making it useful for tree-like structures.
This is Part 2 of the series Software Design Patterns. In Part 1 we covered Creational patterns (Factory, Singleton, Builder, Prototype). Here we focus on Structural patterns — how classes and objects are composed to form larger structures.
We will cover:
1. Adapter Pattern
An Adapter converts one interface into another that clients already expect. Think of a phone charger adapter: same phone port, different wall sockets.
When to use: you must reuse a class whose API does not match your code, and you cannot (or should not) change that class.
interface PaymentProcessor {
void pay(int amountCents);
}
/** Legacy third-party API we cannot change */
class LegacyGateway {
void makePayment(String currency, double amount) {
System.out.println("Paid " + amount + " " + currency);
}
}
class LegacyGatewayAdapter implements PaymentProcessor {
private final LegacyGateway legacy = new LegacyGateway();
@Override
public void pay(int amountCents) {
legacy.makePayment("USD", amountCents / 100.0);
}
}
// Client only knows PaymentProcessor
PaymentProcessor processor = new LegacyGatewayAdapter();
processor.pay(2599);
The client stays clean; all translation lives in the adapter.
2. Decorator Pattern
A Decorator wraps an object to add behavior without changing its class. Java I/O streams (BufferedInputStream wrapping FileInputStream) are classic decorators.
When to use: you need optional features (logging, caching, compression) that can be stacked.
interface Notifier {
void send(String message);
}
class EmailNotifier implements Notifier {
public void send(String message) {
System.out.println("Email: " + message);
}
}
abstract class NotifierDecorator implements Notifier {
protected final Notifier wrappee;
protected NotifierDecorator(Notifier wrappee) { this.wrappee = wrappee; }
public void send(String message) { wrappee.send(message); }
}
class SmsDecorator extends NotifierDecorator {
public SmsDecorator(Notifier n) { super(n); }
public void send(String message) {
super.send(message);
System.out.println("SMS: " + message);
}
}
class SlackDecorator extends NotifierDecorator {
public SlackDecorator(Notifier n) { super(n); }
public void send(String message) {
super.send(message);
System.out.println("Slack: " + message);
}
}
Notifier n = new SlackDecorator(new SmsDecorator(new EmailNotifier()));
n.send("Order shipped");
Stack only the channels you need. Prefer decorator over a deep inheritance tree of EmailAndSmsNotifier, EmailSmsSlackNotifier, …
3. Facade Pattern
A Facade provides a simple front door to a noisy subsystem (many classes, many steps).
When to use: clients keep calling the same sequence of services; you want one method that orchestrates them.
class InventoryService { void reserve(String sku) { /* … */ } }
class PaymentService { void charge(String card, int cents) { /* … */ } }
class ShippingService { void ship(String address) { /* … */ } }
class CheckoutFacade {
private final InventoryService inventory = new InventoryService();
private final PaymentService payment = new PaymentService();
private final ShippingService shipping = new ShippingService();
public void placeOrder(String sku, String card, int cents, String address) {
inventory.reserve(sku);
payment.charge(card, cents);
shipping.ship(address);
}
}
new CheckoutFacade().placeOrder("SKU-1", "4111…", 5000, "Lagos");
Facades do not hide the subsystem forever — advanced callers can still use the individual services. They just make the happy path short.
4. Proxy Pattern
A Proxy stands in for another object and controls access: lazy loading, access control, remote calls, or caching.
interface Report {
String render();
}
class HeavyReport implements Report {
public HeavyReport() { /* expensive load */ }
public String render() { return "big report…"; }
}
class LazyReportProxy implements Report {
private HeavyReport real;
public String render() {
if (real == null) real = new HeavyReport();
return real.render();
}
}
Adapter vs Proxy vs Decorator: Adapter changes the interface; Proxy keeps the same interface and controls access; Decorator keeps the same interface and adds behavior.
5. Composite Pattern
Composite lets you treat individual objects and groups of objects uniformly — trees of menus, files/folders, UI widgets.
interface MenuComponent {
void print(String indent);
}
class MenuItem implements MenuComponent {
private final String name;
MenuItem(String name) { this.name = name; }
public void print(String indent) {
System.out.println(indent + "- " + name);
}
}
class Menu implements MenuComponent {
private final String name;
private final List<MenuComponent> children = new ArrayList<>();
Menu(String name) { this.name = name; }
void add(MenuComponent c) { children.add(c); }
public void print(String indent) {
System.out.println(indent + name);
for (MenuComponent c : children) c.print(indent + " ");
}
}
Menu root = new Menu("File");
root.add(new MenuItem("Open"));
Menu recent = new Menu("Recent");
recent.add(new MenuItem("a.txt"));
root.add(recent);
root.print("");
Final Thought
Structural design patterns help developers build software that is easier to extend, reuse, and maintain by focusing on how objects fit together. The five patterns covered here solve different composition problems, from adapting incompatible interfaces to simplifying complex subsystems and building hierarchical structures.
The most important thing is not memorizing the pattern names, but recognizing the problem each one solves. Use an Adapter when interfaces do not match, a Decorator when behavior needs to be added flexibly, a Facade when a subsystem is unnecessarily complex for clients, a Proxy when access needs to be controlled, and a Composite when individual objects and groups should be treated alike.
Once these patterns become familiar, you can make design decisions based on the structure and needs of the application rather than forcing every problem into an inheritance-based solution.
Next
In Part 3 – Behavioural Patterns we look at how objects communicate: Strategy, Observer, Command, Template Method, and State.
Related reading on this blog: series Part 1 on Creational Patterns.