What structural patterns solve
Structural patterns describe how to combine classes and objects into larger structures while keeping those structures flexible. Where creational patterns (previous lesson) decide how objects are made, structural patterns decide how objects are connected: who wraps whom, who delegates to whom, and how a group of objects can be treated as one.
Most structural patterns are built from the same move: an object holds a reference to another object and forwards calls to it, adding or changing something on the way. That shared shape is why Adapter, Decorator, Proxy and Facade look almost identical in a class diagram — and why interviewers love asking you to tell them apart. The difference is always the intent: converting an interface, adding behaviour, controlling access, or simplifying a subsystem.
In LLD rounds, structural patterns show up as: wrapping a third-party payment SDK behind your own interface (Adapter), adding logging or retries to a service without editing it (Decorator), lazy-loading or caching an expensive resource (Proxy), giving a simple placeOrder entry point over inventory, payment and shipping (Facade), and modelling menus, file systems or organisation charts (Composite). This lesson covers seven patterns in a consistent format — problem, structure, Java code, when to use, when not to, and an interview question — and ends with a comparison table.
Adapter
Problem
You have code that expects interface A, and a class (often from a library or legacy system) that does the right job but exposes interface B. You cannot or should not change either. An adapter sits between them: it implements A and translates each call into calls on B.
The everyday analogy is a travel plug adapter: the socket and the plug both work; they just do not fit each other.
Structure
+--------+ +-----------------+
| Client |----->| <<interface>> |
+--------+ | PaymentGateway | (the Target your code wants)
+--------^--------+
:
+--------+--------+ +-----------------+
| LegacyPayAdapter|------>| LegacyPayClient |
+-----------------+ wraps | (the Adaptee) |
+-----------------+
This is the object adapter (it holds the adaptee). A class adapter extends the adaptee and implements the target; Java's single inheritance makes it rare and less flexible, so prefer the object form.
Java code
Suppose your checkout depends on a clean PaymentGateway interface using paise, while an older vendor client takes rupees as a double and returns a status string:
interface PaymentGateway {
boolean charge(String orderId, long amountPaise);
}
// Third-party or legacy class: cannot be edited.
class LegacyPayClient {
String makePayment(String reference, double rupees) {
return rupees > 0 ? "STATUS=OK" : "STATUS=FAIL";
}
}
class LegacyPayAdapter implements PaymentGateway {
private final LegacyPayClient client;
LegacyPayAdapter(LegacyPayClient client) { this.client = client; }
public boolean charge(String orderId, long amountPaise) {
double rupees = amountPaise / 100.0; // unit translation
String status = client.makePayment(orderId, rupees);
return "STATUS=OK".equals(status); // result translation
}
}
class Checkout {
private final PaymentGateway gateway;
Checkout(PaymentGateway gateway) { this.gateway = gateway; }
String pay(String orderId, long paise) { return gateway.charge(orderId, paise) ? "PAID" : "FAILED"; }
}
class AdapterDemo {
public static void main(String[] args) {
Checkout c = new Checkout(new LegacyPayAdapter(new LegacyPayClient()));
System.out.println(c.pay("ORD-7", 24_900)); // PAID
}
}
The adapter does the dull translation work — units, names, status codes, exceptions — in one place. Checkout never learns that the vendor exists, so switching vendors means writing one more adapter.
JDK examples: Arrays.asList adapts an array to the List interface; InputStreamReader adapts a byte InputStream to a character Reader.
When to use, when not to
- Use to integrate third-party SDKs, legacy code, or anything with an interface you do not control, especially when you may switch providers. In LLD answers this pairs naturally with the dependency-inversion principle: your domain owns the interface; adapters plug vendors into it.
- Avoid when you control both sides — just change one interface. And do not let an adapter grow business logic; it should translate, not decide.
Interview question
"You must support two SMS providers with different APIs. How do you design it?" Define an SmsSender interface in your domain, write one adapter per provider that translates calls and error codes, and inject the chosen adapter. Business code depends only on SmsSender, so adding a third provider is one new adapter.
Decorator
Problem
You want to add responsibilities to an object — logging, caching, compression, retries, extra toppings — dynamically and in combinations, without editing the class and without a subclass for every combination. With three optional features, subclassing needs up to 2³ = 8 classes; with five features, 32. A decorator wraps an object, implements the same interface, and adds behaviour before or after delegating.
Structure
+--------------------------+
| <<interface>> DataSource |<-------------------+
+--------------------------+ |
| + write(data) | | wraps
| + read() | |
+-----------^--------------+ |
: |
+------------+--------------+ |
: : |
+----------------+ +---------------------------+ |
| FileDataSource | | <<abstract>> |------+
| (concrete) | | DataSourceDecorator |
+----------------+ +-------------^-------------+
|
+-------------+-------------+
| |
+-----------------+ +-------------------+
| Encryption | | Compression |
| Decorator | | Decorator |
+-----------------+ +-------------------+
The decorator both is-a DataSource (so it can be used anywhere one is expected) and has-a DataSource (the one it wraps). That combination is what allows stacking.
Java code
A notification example where retries and logging are layered on any sender:
interface Notifier {
void send(String to, String message);
}
class SmsNotifier implements Notifier {
private int calls = 0;
public void send(String to, String message) {
calls++;
if (calls == 1) throw new RuntimeException("gateway timeout"); // first call fails
System.out.println("SMS to " + to + ": " + message);
}
}
abstract class NotifierDecorator implements Notifier {
protected final Notifier inner;
protected NotifierDecorator(Notifier inner) { this.inner = inner; }
}
class LoggingNotifier extends NotifierDecorator {
LoggingNotifier(Notifier inner) { super(inner); }
public void send(String to, String message) {
System.out.println("log: sending to " + to);
inner.send(to, message);
System.out.println("log: sent");
}
}
class RetryingNotifier extends NotifierDecorator {
private final int attempts;
RetryingNotifier(Notifier inner, int attempts) {
super(inner);
this.attempts = attempts;
}
public void send(String to, String message) {
RuntimeException last = null;
for (int i = 1; i <= attempts; i++) {
try {
inner.send(to, message);
return;
} catch (RuntimeException e) {
last = e;
System.out.println("retry " + i + " failed: " + e.getMessage());
}
}
throw last;
}
}
class DecoratorDemo {
public static void main(String[] args) {
Notifier n = new LoggingNotifier(new RetryingNotifier(new SmsNotifier(), 3));
n.send("+91-90000-00000", "OTP 1234");
}
}
Output:
log: sending to +91-90000-00000
retry 1 failed: gateway timeout
SMS to +91-90000-00000: OTP 1234
log: sent
The order of wrapping matters. Logging(Retrying(Sms)) logs once around all attempts; Retrying(Logging(Sms)) would log every attempt. Being able to explain that is a good signal in interviews.
The real example: java.io
Java's I/O library is the textbook decorator. InputStream is the component interface (an abstract class). FileInputStream and ByteArrayInputStream are concrete sources. FilterInputStream is the base decorator, and BufferedInputStream, DataInputStream and GZIPInputStream are decorators that each add one capability:
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
class JavaIoDecoratorDemo {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(new GZIPOutputStream(bytes))) {
out.writeInt(42);
out.writeUTF("hello");
}
try (DataInputStream in = new DataInputStream( // adds readInt/readUTF
new BufferedInputStream( // adds buffering
new GZIPInputStream( // adds decompression
new ByteArrayInputStream(bytes.toByteArray()))))) { // source
System.out.println(in.readInt() + " " + in.readUTF()); // 42 hello
}
}
}
Each wrapper is an InputStream, so you compose exactly the features you need. The downside is also visible: many small classes, and a long nested constructor chain that beginners find confusing.
Python's function decorators (@lru_cache, @retry) share the idea — wrap a callable to add behaviour — though they wrap functions rather than objects implementing an interface.
When to use, when not to
- Use for optional, combinable cross-cutting behaviour: logging, metrics, caching, retries, authorisation checks, rate limiting, formatting layers, price add-ons (a coffee with milk and extra shot).
- Avoid when the interface is large — every decorator must forward every method, which is tedious and error-prone — or when the added behaviour depends on the concrete type being wrapped. Deep stacks are also harder to debug because a stack trace passes through many wrappers.
Interview question
"Design pricing for a coffee shop where customers can add milk, sugar and an extra shot in any combination." A Beverage interface with cost() and description(), concrete Espresso, and decorators Milk, Sugar, ExtraShot that wrap a Beverage and add their price. new Milk(new ExtraShot(new Espresso())) composes any combination without a class per combination.
Facade
Problem
A subsystem has many classes with detailed APIs. Most clients only need one or two common operations, but must currently know the right order of calls across several classes. A facade offers a simple, higher-level interface over the subsystem. It does not hide the subsystem completely — advanced clients can still use it directly.
Structure
+--------+ +-------------------+
| Client |------>| OrderFacade |
+--------+ +-------------------+
| + placeOrder(...) |
+----+-----+-----+--+
| | |
+----------+ | +-----------+
v v v
+-----------+ +-----------+ +-----------+
| Inventory | | Payment | | Shipping |
+-----------+ +-----------+ +-----------+
Java code
class Inventory {
boolean reserve(String sku, int qty) { System.out.println("reserved " + qty + " x " + sku); return true; }
void release(String sku, int qty) { System.out.println("released " + sku); }
}
class Payments {
boolean charge(String user, long paise) { System.out.println("charged " + paise); return paise < 1_000_000; }
}
class Shipping {
String schedule(String user, String sku) { return "SHIP-" + sku; }
}
class OrderFacade {
private final Inventory inventory;
private final Payments payments;
private final Shipping shipping;
OrderFacade(Inventory i, Payments p, Shipping s) {
this.inventory = i;
this.payments = p;
this.shipping = s;
}
String placeOrder(String user, String sku, int qty, long paise) {
if (!inventory.reserve(sku, qty)) return "OUT_OF_STOCK";
if (!payments.charge(user, paise)) {
inventory.release(sku, qty); // the facade owns the compensation order
return "PAYMENT_FAILED";
}
return shipping.schedule(user, sku);
}
}
class FacadeDemo {
public static void main(String[] args) {
OrderFacade f = new OrderFacade(new Inventory(), new Payments(), new Shipping());
System.out.println(f.placeOrder("u1", "BOOK-1", 1, 49_900));
}
}
The client calls one method. The facade knows the sequence, including the rollback when payment fails. The logging library SLF4J (Simple Logging Facade for Java) is named after the pattern: one small API in front of several logging back ends. In everyday code, any "service" class that orchestrates several components is a facade.
When to use, when not to
- Use to give a simple entry point to a complex subsystem, to decouple clients from subsystem churn, or to define layers (each layer talks to the next layer's facade).
- Avoid letting the facade become a god class that every feature is bolted onto. If it grows dozens of methods, split it into several facades by use case. Also avoid adding a facade over a subsystem that is already simple.
Interview question
"What is the difference between a facade and an adapter?" A facade defines a new, simpler interface over many classes to make them easier to use. An adapter makes one existing class fit an existing interface that clients already expect. Facade simplifies; adapter converts.
Proxy
Problem
You want to control access to an object — delay its creation, check permissions, call it across a network, or cache its results — without clients knowing. A proxy implements the same interface as the real object (the real subject) and decides when and whether to forward each call.
Structure
+--------+ +---------------------+
| Client |----->| <<interface>> Image |
+--------+ +----------^----------+
:
+------------+------------+
: :
+------------------+ +------------------+
| ImageProxy |----->| RealImage |
| (controls access)| holds| (expensive) |
+------------------+ +------------------+
The four common kinds
| Kind | What it controls | Example |
|---|---|---|
| Virtual proxy | Creation: creates the real object lazily, on first use | Load a large image only when displayed; Hibernate lazy-loaded entities |
| Protection proxy | Access: checks permissions before forwarding | Only admins may call deleteUser |
| Remote proxy | Location: makes a remote object look local | Java RMI stubs; gRPC client stubs |
| Caching proxy | Repetition: returns stored results for repeated calls | Cache exchange-rate lookups for 60 seconds |
Java code
Virtual, protection and caching proxies in one example:
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
interface ReportService {
String report(String user, String month);
}
class RealReportService implements ReportService {
RealReportService() { System.out.println("(expensive start-up: loading data)"); }
public String report(String user, String month) {
System.out.println("(computing report for " + month + ")");
return "Sales for " + month + ": 1200 orders";
}
}
// Virtual proxy: delays the expensive constructor until first real use.
class LazyReportService implements ReportService {
private ReportService real;
public synchronized String report(String user, String month) {
if (real == null) real = new RealReportService();
return real.report(user, month);
}
}
// Protection proxy: only allowed users get through.
class SecuredReportService implements ReportService {
private final ReportService inner;
private final Set<String> allowed;
SecuredReportService(ReportService inner, Set<String> allowed) {
this.inner = inner;
this.allowed = allowed;
}
public String report(String user, String month) {
if (!allowed.contains(user)) throw new SecurityException(user + " may not view reports");
return inner.report(user, month);
}
}
// Caching proxy: repeated months are served from memory.
class CachedReportService implements ReportService {
private final ReportService inner;
private final Map<String, String> cache = new HashMap<>();
CachedReportService(ReportService inner) { this.inner = inner; }
public synchronized String report(String user, String month) {
return cache.computeIfAbsent(month, m -> inner.report(user, m));
}
}
class ProxyDemo {
public static void main(String[] args) {
ReportService svc = new SecuredReportService(
new CachedReportService(new LazyReportService()), Set.of("asha"));
System.out.println("built proxies");
System.out.println(svc.report("asha", "2026-09"));
System.out.println(svc.report("asha", "2026-09")); // cached: no recompute
try {
svc.report("ravi", "2026-09");
} catch (SecurityException e) {
System.out.println(e.getMessage());
}
}
}
Running it prints built proxies first — the expensive service has not been created yet — then the start-up and computing messages once, then the cached result, then the access-denied message. Note the cache is keyed only by month, which is correct here because the report does not depend on the user; if it did, the key would need to include the user.
A remote proxy would implement ReportService by serialising the arguments, sending an HTTP or gRPC request, and deserialising the reply. Code generated by gRPC or an HTTP client library like Retrofit or Feign plays exactly this role.
Java also supports dynamic proxies (java.lang.reflect.Proxy), which generate a proxy class at runtime for any interface; frameworks like Spring use these (or bytecode generation) to add transactions and security around your beans.
When to use, when not to
- Use for lazy loading of heavy resources, access control at a clear boundary, making remote calls look local, and caching expensive calls.
- Avoid when the indirection hides important costs — a remote proxy that looks like a cheap local call tempts callers into chatty loops of network calls. Also be careful with caching proxies: you now own cache invalidation and staleness.
Interview question
"How is a proxy different from a decorator if both wrap an object with the same interface?" The structure is the same; the intent differs. A decorator adds behaviour and is usually composed by the client in stackable layers. A proxy controls access to the real subject — when it is created, who may call it, where it lives — and often manages the real subject's lifecycle itself, so the client may not know a proxy is there.
Composite
Problem
You have tree-shaped data where individual items and groups of items should be treated the same way. A file system has files and folders; a folder's size is the sum of its children's sizes, and folders can contain folders. A menu has items and submenus. An organisation has employees and teams. Without a pattern, client code is full of if (node is folder) loop else ....
Composite gives leaves and containers a common interface; containers hold children of that interface type and implement operations by delegating to their children.
Structure
+------------------------------+
| <<interface>> FileSystemNode |<-------+
+------------------------------+ |
| + name(): String | | children
| + size(): long | | 0..*
+-------------^----------------+ |
: |
+-------------+-------------+ |
: : |
+-----------------+ +-----------------+ |
| File (leaf) | | Folder |<>+
+-----------------+ | (composite) |
| - bytes: long | +-----------------+
+-----------------+ | + add(node) |
+-----------------+
Java code
import java.util.ArrayList;
import java.util.List;
interface FileSystemNode {
String name();
long size();
void print(String indent);
}
record FileNode(String name, long size) implements FileSystemNode {
public void print(String indent) { System.out.println(indent + name + " (" + size + ")"); }
}
class Folder implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
Folder(String name) { this.name = name; }
Folder add(FileSystemNode child) { children.add(child); return this; }
public String name() { return name; }
public long size() {
long total = 0;
for (FileSystemNode c : children) total += c.size(); // recursion via polymorphism
return total;
}
public void print(String indent) {
System.out.println(indent + name + "/ (" + size() + ")");
for (FileSystemNode c : children) c.print(indent + " ");
}
}
class CompositeDemo {
public static void main(String[] args) {
Folder root = new Folder("root")
.add(new FileNode("notes.txt", 120))
.add(new Folder("photos")
.add(new FileNode("a.jpg", 2_000))
.add(new FileNode("b.jpg", 3_000)))
.add(new Folder("empty"));
root.print("");
}
}
Output, with sizes 120 + (2000 + 3000) + 0 = 5120:
root/ (5120)
notes.txt (120)
photos/ (5000)
a.jpg (2000)
b.jpg (3000)
empty/ (0)
A design choice to mention: where do add and remove live? Putting them only on Folder (as here) is type-safe — you cannot add a child to a file — but clients must know whether they hold a folder. Putting them on the shared interface is transparent — all nodes look the same — but leaves must throw on add, which is an LSP smell. Most modern code prefers the type-safe version.
When to use, when not to
- Use for genuine part-whole hierarchies where clients should treat single items and groups uniformly: file systems, UI component trees, menus, organisation charts, expression trees, bundles of products priced as one.
- Avoid when the structure is flat, or when leaves and composites have very different operations — forcing them into one interface produces many methods that do not make sense for one side.
Interview question
"Design a menu where a combo meal contains items and other combos, and price is computed with a 10% combo discount." A MenuComponent interface with price(); MenuItem returns its own price; Combo holds children and returns 90% of the sum of its children's prices. Nested combos work automatically through recursion, and callers just call price() on whatever they hold.
Bridge
Problem
You have two independent dimensions of variation, and inheritance would multiply them. Notifications vary by type (alert, reminder, promotion) and by channel (SMS, email, push). Subclassing gives AlertSms, AlertEmail, ReminderSms... — 3 × 3 = 9 classes, and 12 when a fourth channel arrives. Bridge splits the two dimensions into two hierarchies — the abstraction and the implementation — and connects them by composition, so you need 3 + 3 classes.
Structure
Abstraction side Implementation side
+-------------------+ has-a +----------------------+
| <<abstract>> |---------->| <<interface>> |
| Notification | "bridge" | Channel |
+---------^---------+ +----------^-----------+
| :
+-----+------+ +------+------+
| | : :
+-------+ +----------+ +---------+ +---------+
| Alert | | Reminder | | Sms | | Email |
+-------+ +----------+ +---------+ +---------+
Java code
interface Channel {
void deliver(String to, String text);
}
class SmsChannel implements Channel {
public void deliver(String to, String text) { System.out.println("SMS " + to + ": " + text); }
}
class EmailChannel implements Channel {
public void deliver(String to, String text) { System.out.println("EMAIL " + to + ": " + text); }
}
abstract class Notification {
protected final Channel channel; // the bridge
protected Notification(Channel channel) { this.channel = channel; }
abstract void send(String to, String detail);
}
class Alert extends Notification {
Alert(Channel c) { super(c); }
void send(String to, String detail) { channel.deliver(to, "[ALERT] " + detail.toUpperCase()); }
}
class Reminder extends Notification {
Reminder(Channel c) { super(c); }
void send(String to, String detail) { channel.deliver(to, "Reminder: " + detail); }
}
class BridgeDemo {
public static void main(String[] args) {
new Alert(new SmsChannel()).send("+91-90000-00000", "card used abroad");
new Reminder(new EmailChannel()).send("a@x.in", "EMI due on 5th");
}
}
Adding a push channel is one class; adding a "promotion" type is one class. JDBC is a well-known real example: your code talks to the Connection/Statement abstraction, and each database driver provides the implementation side.
When to use, when not to
- Use when two dimensions vary independently and you see a class explosion of combinations, or when the implementation should be switchable at runtime.
- Avoid with only one dimension of variation; then a plain interface (Strategy) is enough. Bridge is often recognised only once the explosion has started — that is fine; it is a refactoring target.
Interview question
"How is Bridge different from Strategy, since both hold an interface?" Structurally they are close. Strategy is about swapping one algorithm inside a context. Bridge is about letting two hierarchies — an abstraction with its own subclasses and an implementation with its own — vary independently, avoiding an M × N class explosion.
Flyweight
Problem
You need a huge number of similar objects, and memory becomes the bottleneck. Much of each object's data is identical across many objects. Flyweight separates state into:
- Intrinsic state: shared, context-independent, immutable — stored once in a flyweight object.
- Extrinsic state: varies per use — supplied by the caller or stored in a small per-instance object.
Example: a map with 1,000,000 trees. Each tree has a position (unique) and a species with name, colour and texture (shared by thousands of trees).
Structure
+-----------+ 1..* 1 +--------------------+
| Tree |------------>| TreeType (shared) |
+-----------+ +--------------------+
| - x, y | | - species |
| extrinsic | | - colour, texture |
+-----------+ | (intrinsic) |
+---------^----------+
| creates and caches
+-------------+------------+
| TreeTypeFactory (cache) |
+--------------------------+
Java code
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
record TreeType(String species, String colour) {} // intrinsic, immutable, shared
record Tree(int x, int y, TreeType type) {} // extrinsic position + shared reference
class TreeTypeFactory {
private final Map<String, TreeType> cache = new HashMap<>();
TreeType get(String species, String colour) {
return cache.computeIfAbsent(species + "|" + colour, k -> new TreeType(species, colour));
}
int distinctTypes() { return cache.size(); }
}
class FlyweightDemo {
public static void main(String[] args) {
TreeTypeFactory factory = new TreeTypeFactory();
List<Tree> forest = new ArrayList<>();
String[] species = {"neem", "mango", "banyan"};
for (int i = 0; i < 300_000; i++) {
forest.add(new Tree(i % 1000, i / 1000, factory.get(species[i % 3], "green")));
}
System.out.println(forest.size() + " trees, " + factory.distinctTypes() + " shared types");
}
}
It prints 300000 trees, 3 shared types. If each TreeType held, say, a 10 KB texture, sharing three of them instead of storing 300,000 copies avoids roughly 3 GB of duplicated data (300,000 × 10 KB = 3,000,000 KB).
JDK examples: Integer.valueOf returns cached Integer objects for values from -128 to 127 (the JLS requires at least this range); string literals are interned and shared; Boolean.valueOf returns one of two shared objects.
Common mistake
Flyweights must be immutable. If one tree could change the colour on its shared TreeType, every tree of that species would change. Also note that Integer caching is why Integer a = 127, b = 127; a == b is true while the same with 128 is usually false — always compare boxed numbers with equals.
When to use, when not to
- Use when you have very many objects, memory is a measured problem, and a large part of each object's state can be shared: characters in a text editor, map tiles, game particles, seat-type metadata in a stadium.
- Avoid when object counts are modest — the factory, the split of state and the extra lookups add complexity for no gain.
Interview question
"A theatre booking system stores 50,000 seats per venue, each with category, price band and amenities. How do you reduce memory?" Extract category, price band and amenities into an immutable shared SeatType obtained from a factory cache; each Seat keeps only its row, number and status plus a reference to its SeatType. Status stays per seat because it changes.
Adapter vs Decorator vs Proxy vs Facade
All four wrap something and forward calls. Here is how to tell them apart:
| Adapter | Decorator | Proxy | Facade | |
|---|---|---|---|---|
| Intent | Convert one interface to another | Add behaviour dynamically | Control access to an object | Simplify a subsystem |
| Interface exposed | A different interface from the wrapped object | The same interface | The same interface | A new, simpler interface |
| Wraps | One object | One object (stackable) | One object | Many objects |
| Who creates the wrapped object | Usually passed in | Passed in by the client | Often the proxy itself (lazy) | The facade or its constructor |
| Client aware of wrapping? | Usually yes, at wiring time | Yes, it composes layers | Often no | Yes, it chooses to use the facade |
| Typical examples | Vendor SDK behind your interface; InputStreamReader | BufferedInputStream; logging and retry layers | Lazy loading; access checks; RPC stubs; caching | OrderFacade.placeOrder; a service layer |
A one-line memory aid for interviews: adapter changes the interface, decorator adds features, proxy controls access, facade simplifies.
Interview tip
When asked to compare them, start with intent, then point out that decorator and proxy share the same structure, so the difference is purpose and who controls the wrapped object's lifecycle. This shows you understand patterns as solutions to problems rather than as class shapes.
Interview questions
Q1. What is the Adapter pattern and where would you use it in an LLD problem?
An adapter implements the interface your code expects and translates calls to an existing class with a different interface. In LLD problems it typically wraps payment, SMS or map-provider SDKs behind a domain interface such as PaymentGateway. The domain stays clean, and switching providers means writing another adapter.
Q2. Object adapter vs class adapter?
An object adapter holds a reference to the adaptee and delegates; a class adapter extends the adaptee and implements the target interface. In Java, single inheritance makes class adapters inflexible — they cannot adapt subclasses of the adaptee and expose all of its public methods — so object adapters are the norm.
Q3. Explain Decorator using java.io.
InputStream is the shared type, FileInputStream is a concrete source, and BufferedInputStream, DataInputStream and GZIPInputStream each wrap another InputStream to add buffering, typed reads or decompression. Because each wrapper is itself an InputStream, you stack exactly the features you need without a subclass for every combination.
Q4. Why does the order of decorators matter?
Each decorator runs its logic around the next layer, so order changes behaviour. A logging decorator outside a retry decorator logs once per request; inside it, it logs every attempt. Similarly, compressing then encrypting differs from encrypting then compressing, and only one of those achieves any compression.
Q5. What kinds of proxies exist?
Virtual proxies delay creating expensive objects until needed; protection proxies check permissions; remote proxies make a remote object look local; caching proxies return stored results for repeated requests. Others include logging and smart-reference proxies. All share the real subject's interface.
Q6. Facade vs Adapter?
A facade defines a new, simpler interface over a set of classes to make common tasks easy. An adapter makes one existing class conform to an interface clients already depend on. A facade reduces complexity; an adapter resolves incompatibility.
Q7. When should a facade be avoided?
When the subsystem is already simple, or when the facade would grow into a god class with every operation added to it. If a facade keeps accumulating methods, split it by use case. A facade should not prevent advanced clients from using the subsystem directly if they need to.
Q8. Explain the Composite pattern with an example.
Composite lets clients treat individual objects and groups uniformly through a shared interface. In a file system, File and Folder both implement size(); a folder sums its children's sizes, and children can be folders, so recursion happens naturally. Client code calls size() without checking types.
Q9. Should add and remove be on the component interface in Composite?
Putting them on the interface makes leaves and composites fully interchangeable but forces leaves to throw, which violates Liskov substitution. Putting them only on the composite is type-safe but means clients must know what they hold. Most designs choose type safety.
Q10. What problem does Bridge solve?
It prevents an M × N class explosion when two dimensions vary independently, such as notification type and delivery channel. The abstraction hierarchy holds a reference to an implementation interface, so you add a type or a channel as a single class. JDBC is a common real-world example.
Q11. What are intrinsic and extrinsic state in Flyweight?
Intrinsic state is shared, context-independent and immutable, stored once in the flyweight, like a tree species' texture. Extrinsic state varies per use, like a tree's position, and is supplied by the caller or kept in a small per-object record. Separating them lets millions of objects share a handful of flyweights.
Q12. Give JDK examples of structural patterns.
Adapter: Arrays.asList, InputStreamReader. Decorator: BufferedInputStream, Collections.unmodifiableList (adds a restriction while keeping the interface). Proxy: java.lang.reflect.Proxy dynamic proxies. Flyweight: Integer.valueOf caching and string interning.
Q13. How do you add logging to every method of a service without editing it?
Write a decorator that implements the same interface, logs, and delegates to the real service, then wire it in at the composition root. For many interfaces at once, a dynamic proxy or a framework's aspect support generates such wrappers. Either way the service class remains unchanged, which respects the open/closed principle.
Key takeaways
- Structural patterns connect objects; most work by wrapping and forwarding.
- Adapter converts an existing interface into the one your code expects; ideal for third-party SDKs.
- Decorator adds stackable behaviour behind the same interface;
java.iostreams are the classic example, and order matters. - Proxy controls access — lazy creation, permissions, remote calls, caching — behind the same interface.
- Facade gives one simple entry point over several classes without forbidding direct use.
- Composite treats leaves and groups uniformly in trees; prefer type-safe child management.
- Bridge separates two independent dimensions to avoid M × N subclasses; Flyweight shares immutable intrinsic state to save memory.
- Distinguish look-alike patterns by intent: convert, add, control, simplify.
Next lesson
Continue with Behavioural design patterns.

