Structural Patterns
Adapter, Decorator, Facade, Composite, Proxy, Bridge, and Flyweight — how to compose objects into larger structures.
Introduction
Structural design patterns explain how classes and objects are assembled into larger shapes without turning every class into a tightly coupled dependency graph.
In LLD interviews at Amazon, Microsoft, and Netflix, these patterns show up whenever the design has third-party integrations, optional features, tree-shaped data, expensive objects, remote resources, or multiple dimensions of variation. The core skill is not naming the pattern. The core skill is recognizing the coupling problem and choosing the smallest structure that keeps the design extensible.
Learning Objectives
Explain the intent of Adapter, Decorator, Facade, Composite, Proxy, Bridge, and Flyweight.
Write compact Java examples that demonstrate each structural pattern.
Map each pattern to real production use cases and common LLD interview problems.
Compare Decorator, Proxy, and Adapter by the kind of boundary they manage.
Compare Bridge and Adapter by whether variation is designed up front or repaired later.
Use structural patterns without hiding domain rules inside accidental wrappers.
Core Theory
Structural patterns organize object relationships
A structural pattern should answer one of these questions:
- How do I make an incompatible class usable without changing it?
- How do I add behavior without subclass explosion?
- How do I hide a complex subsystem behind a simple API?
- How do I represent part-whole hierarchies uniformly?
- How do I control access to a real object?
- How do I separate abstraction from implementation when both vary?
- How do I share many small immutable objects efficiently?
In interviews, introduce the pattern only after explaining the design pressure. That makes the pattern feel like a consequence of requirements, not memorized vocabulary.
Adapter: make incompatible interfaces work together
Intent: Convert the interface of an existing class into the interface your domain expects, without changing the existing class.
Real use case: A checkout service exposes one PaymentGateway contract while Stripe, Razorpay, PayPal, or a bank SDK has different method names and request objects.
In LLD problems: Use Adapter when your clean domain model must talk to a legacy class, third-party SDK, vendor API, or old internal module. In a notification system, adapters can normalize email, SMS, and push providers behind one NotificationChannel contract.
public interface PaymentGateway {
PaymentReceipt charge(String orderId, long amountInCents);
}
public final class LegacyBankClient {
public String makePayment(String reference, int amountInPaise) {
return "BANK-" + reference + "-" + amountInPaise;
}
}
public final class BankPaymentAdapter implements PaymentGateway {
private final LegacyBankClient legacyClient;
public BankPaymentAdapter(LegacyBankClient legacyClient) {
this.legacyClient = legacyClient;
}
@Override
public PaymentReceipt charge(String orderId, long amountInCents) {
String bankId = legacyClient.makePayment(orderId, Math.toIntExact(amountInCents));
return new PaymentReceipt(orderId, bankId, true);
}
}
public record PaymentReceipt(String orderId, String providerReference, boolean success) {
}Decorator: add responsibilities around the same contract
Intent: Wrap an object with another object that implements the same interface, adding behavior before or after delegating to the wrapped object.
Real use case: HTTP clients are commonly decorated with retry, metrics, tracing, compression, authentication, and rate limiting without changing the base client.
In LLD problems: Use Decorator when features are optional and composable. In coffee machine, toppings decorate a base beverage. In logging framework, timestamp, masking, sampling, or async behavior can decorate a logger while preserving the Logger interface.
public interface Notifier {
void send(String userId, String message);
}
public final class EmailNotifier implements Notifier {
@Override
public void send(String userId, String message) {
System.out.println("email to " + userId + ": " + message);
}
}
public abstract class NotifierDecorator implements Notifier {
private final Notifier delegate;
protected NotifierDecorator(Notifier delegate) {
this.delegate = delegate;
}
@Override
public void send(String userId, String message) {
delegate.send(userId, message);
}
}
public final class SmsNotifierDecorator extends NotifierDecorator {
public SmsNotifierDecorator(Notifier delegate) {
super(delegate);
}
@Override
public void send(String userId, String message) {
super.send(userId, message);
System.out.println("sms to " + userId + ": " + message);
}
}Facade: provide a simple entry point over a subsystem
Intent: Offer a coarse-grained API that hides the coordination details of several collaborating classes.
Real use case: A media upload service can expose uploadVideo while internally validating metadata, storing the raw file, transcoding variants, generating thumbnails, and publishing events.
In LLD problems: Use Facade when the client should not orchestrate many subsystem calls. In hotel booking, a BookingFacade can coordinate room inventory, pricing, payment, and confirmation without making controllers know every service.
public final class OrderFacade {
private final InventoryService inventory;
private final PaymentService payments;
private final ShipmentService shipments;
public OrderFacade(InventoryService inventory, PaymentService payments, ShipmentService shipments) {
this.inventory = inventory;
this.payments = payments;
this.shipments = shipments;
}
public OrderConfirmation placeOrder(String sku, int quantity, long amountInCents) {
inventory.reserve(sku, quantity);
String paymentId = payments.charge(amountInCents);
String shipmentId = shipments.createShipment(sku, quantity);
return new OrderConfirmation(paymentId, shipmentId);
}
}
public interface InventoryService { void reserve(String sku, int quantity); }
public interface PaymentService { String charge(long amountInCents); }
public interface ShipmentService { String createShipment(String sku, int quantity); }
public record OrderConfirmation(String paymentId, String shipmentId) { }Composite: treat individual objects and groups uniformly
Intent: Compose objects into tree structures and let callers use leaves and groups through the same component interface.
Real use case: File systems, menus, organization charts, product bundles, expression trees, and nested comments all have part-whole structure.
In LLD problems: Use Composite when the problem has folders and files, tasks and subtasks, categories and products, or comments and replies. It lets traversal, pricing, permission checks, and rendering work recursively without type checks at every level.
import java.util.ArrayList;
import java.util.List;
public interface FileSystemNode {
String name();
long sizeInBytes();
}
public final class FileNode implements FileSystemNode {
private final String name;
private final long sizeInBytes;
public FileNode(String name, long sizeInBytes) {
this.name = name;
this.sizeInBytes = sizeInBytes;
}
public String name() { return name; }
public long sizeInBytes() { return sizeInBytes; }
}
public final class FolderNode implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
public FolderNode(String name) {
this.name = name;
}
public void add(FileSystemNode child) { children.add(child); }
public String name() { return name; }
public long sizeInBytes() {
return children.stream().mapToLong(FileSystemNode::sizeInBytes).sum();
}
}Proxy: control access to another object
Intent: Stand in front of a real subject and control access, lifecycle, caching, security, rate limiting, or remote communication while preserving the same interface.
Real use case: Repository proxies add caching, service proxies hide network calls, image proxies lazy-load heavy images, and authorization proxies check permissions before invoking a protected service.
In LLD problems: Use Proxy when callers should depend on the same contract but access must be mediated. In a rate limiter, a proxy can guard an API client. In an authentication service, a proxy can check tokens before delegating to account operations.
import java.util.HashMap;
import java.util.Map;
public interface UserDirectory {
UserProfile findById(String userId);
}
public final class DatabaseUserDirectory implements UserDirectory {
@Override
public UserProfile findById(String userId) {
return new UserProfile(userId, "loaded from database");
}
}
public final class CachedUserDirectoryProxy implements UserDirectory {
private final UserDirectory delegate;
private final Map<String, UserProfile> cache = new HashMap<>();
public CachedUserDirectoryProxy(UserDirectory delegate) {
this.delegate = delegate;
}
@Override
public UserProfile findById(String userId) {
return cache.computeIfAbsent(userId, delegate::findById);
}
}
public record UserProfile(String userId, String source) {
}Bridge: separate abstraction from implementation
Intent: Split a hierarchy into two dimensions so the abstraction and implementation can vary independently.
Real use case: A notification abstraction such as Alert can vary by alert type while delivery varies by email, SMS, Slack, or push. Adding a new alert should not require subclasses for every channel combination.
In LLD problems: Use Bridge when you see two independent axes such as device and remote, shape and renderer, message type and channel, payment flow and provider, or report and exporter. It avoids multiplying classes like UrgentEmailAlert, UrgentSmsAlert, and DigestEmailAlert.
public interface MessageSender {
void send(String destination, String body);
}
public final class EmailSender implements MessageSender {
@Override
public void send(String destination, String body) {
System.out.println("email " + destination + ": " + body);
}
}
public abstract class Alert {
private final MessageSender sender;
protected Alert(MessageSender sender) {
this.sender = sender;
}
protected void deliver(String destination, String body) {
sender.send(destination, body);
}
public abstract void notify(String destination);
}
public final class CriticalAlert extends Alert {
public CriticalAlert(MessageSender sender) {
super(sender);
}
@Override
public void notify(String destination) {
deliver(destination, "critical incident detected");
}
}Flyweight: share intrinsic state across many objects
Intent: Reuse immutable shared state for many fine-grained objects and keep unique extrinsic state outside the shared object.
Real use case: Text editors share glyph metadata, maps share marker icons, games share sprites, and inventory systems share product definitions while each occurrence stores only position, quantity, or owner-specific state.
In LLD problems: Use Flyweight when the design creates thousands or millions of similar objects. In chess or ludo, piece type metadata can be shared. In a text editor, each character occurrence can reference a shared glyph style instead of duplicating font data.
import java.util.HashMap;
import java.util.Map;
public final class ProductType {
private final String sku;
private final String name;
private final int weightGrams;
public ProductType(String sku, String name, int weightGrams) {
this.sku = sku;
this.name = name;
this.weightGrams = weightGrams;
}
public String sku() { return sku; }
public String name() { return name; }
public int weightGrams() { return weightGrams; }
}
public final class ProductTypeFactory {
private final Map<String, ProductType> cache = new HashMap<>();
public ProductType get(String sku, String name, int weightGrams) {
return cache.computeIfAbsent(sku, key -> new ProductType(key, name, weightGrams));
}
}
public record InventoryItem(String binId, int quantity, ProductType type) {
}Diagrams
Composite pattern for a file system
Comparisons
Decorator vs Proxy vs Adapter
| Dimension | Decorator | Proxy | Adapter | Decision clue |
|---|---|---|---|---|
| Primary purpose | Add behavior around the same contract | Control access to a real subject | Convert one interface into another | Ask whether the issue is extra behavior, mediated access, or incompatible API |
| Interface shape | Same as wrapped component | Same as real subject | Target interface differs from adaptee interface | Adapter is the only one whose main job is interface translation |
| Typical behavior | Retry, metrics, compression, masking, extra notification channel | Caching, lazy loading, authorization, rate limiting, remote stub | Request mapping, response mapping, unit conversion, method renaming | Name the wrapper by what boundary it manages |
| LLD example | Toppings around Beverage or enrichers around Logger | Cached repository or protected account service | Payment provider SDK behind PaymentGateway | Use the simplest wrapper that matches the requirement |
| Risk | Too many nested wrappers can hide order-sensitive behavior | Proxy can become a policy dumping ground | Adapter can leak vendor concepts into the domain | Keep wrappers small and keep domain rules explicit |
Bridge vs Adapter
| Dimension | Bridge | Adapter | Decision clue |
|---|---|---|---|
| When applied | Designed up front when two dimensions vary independently | Added when an existing interface does not match the desired interface | Bridge prevents class explosion; Adapter repairs incompatibility |
| Main structure | Abstraction holds an implementation interface | Adapter implements the target interface and delegates to adaptee | Bridge splits hierarchy; Adapter wraps a mismatched collaborator |
| Change supported | New abstractions and implementations can be combined independently | New adaptees can be supported by writing new adapters | Use Bridge for planned variation and Adapter for external boundaries |
| LLD example | Alert type varies independently from delivery channel | Bank SDK is adapted to PaymentGateway | If both axes are yours, consider Bridge; if one side is foreign, use Adapter |
Where each structural pattern appears in LLD
| Pattern | Design pressure | Common problem |
|---|---|---|
| Adapter | Third-party or legacy interface mismatch | Payment gateway, notification system, shopping cart |
| Decorator | Optional behavior combinations | Coffee machine, logging framework, notification system |
| Facade | Complex subsystem orchestration | Hotel booking, movie booking, food delivery |
| Composite | Tree of parts and groups | File system, text editor, trello |
| Proxy | Controlled access to real object | Rate limiter, authentication service, API gateway |
| Bridge | Two independent variation axes | Notification system, payment gateway, report exporter |
| Flyweight | Large count of similar objects | Text editor, chess, inventory management |
Best Practices
- ✓
Start from the coupling or extensibility problem, then name the pattern.
- ✓
Keep wrappers thin. Decorators, proxies, and adapters should not become hidden god objects.
- ✓
Use interfaces when a wrapper must be substitutable for the wrapped object.
- ✓
Prefer composition over inheritance for Decorator, Proxy, Bridge, and Facade collaborations.
- ✓
Keep Adapter mapping at the system boundary so vendor request and response shapes do not leak inward.
- ✓
Use Facade to simplify callers, not to bypass domain services or hide all errors.
- ✓
For Composite, make recursive operations explicit and decide how cycles, ordering, and permissions are handled.
- ✓
For Flyweight, clearly separate intrinsic shared state from extrinsic per-instance state.
Common Mistakes
- ×
Calling any wrapper an Adapter even when the interface does not change.
- ×
Using Decorator for mutually exclusive choices that should be Strategy or State.
- ×
Letting a Facade contain all business logic instead of delegating to cohesive services.
- ×
Building Composite without a clear leaf and group contract, forcing callers back into type checks.
- ×
Using Proxy to silently change business semantics instead of controlling access or lifecycle.
- ×
Using Bridge when a simple interface injection would be enough for one variation axis.
- ×
Applying Flyweight to mutable shared state, causing one object update to corrupt many logical instances.
- ×
Overexplaining pattern names while underexplaining the domain responsibility of each class.
Quiz
0/8 answered
1.Which pattern should you choose when a vendor SDK has methods that do not match your internal PaymentGateway interface?
2.Which pattern best fits optional behavior such as adding metrics and retry around the same HTTP client contract?
3.What is the main benefit of Facade in an LLD design?
4.A folder and a file should both support sizeInBytes, while a folder contains more nodes. Which pattern fits?
5.Which pattern should control lazy loading, caching, or authorization while keeping the same interface as the real object?
6.Bridge is most useful when:
7.What must be true for Flyweight to be safe?
8.Which statement best distinguishes Adapter from Bridge?
Flashcards
Cheat Sheet
Adapter: Convert a legacy, vendor, or incompatible interface into the domain contract. Use at boundaries.
Decorator: Add optional behavior around the same interface. Use for stackable features such as retry, metrics, masking, toppings, and extra channels.
Facade: Hide multi-service orchestration behind one simple API. Use for checkout, booking, upload, or onboarding flows.
Composite: Represent leaves and groups uniformly. Use for file systems, menus, bundles, comments, tasks, and organization trees.
Proxy: Preserve the same interface while controlling access to a real object. Use for cache, lazy load, auth, rate limiting, and remote stubs.
Bridge: Split abstraction from implementation when two axes vary independently. Use for message type plus channel, shape plus renderer, or payment flow plus provider.
Flyweight: Share immutable intrinsic state and keep per-instance extrinsic state outside. Use when object count is very high.
Interview rule: Explain the design pressure first, then the pattern. If the pattern does not reduce coupling, simplify the design.
References
- BookDesign Patterns: Elements of Reusable Object-Oriented Software — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
- BookHead First Design Patterns — Eric Freeman and Elisabeth Robson
- BlogRefactoring Guru: Structural Design Patterns
- DocsOracle Java Tutorials: Interfaces