Structural Patterns — Cheat Sheet
Low Level Design · 8 topics. Download the PDF or the Instagram carousel and share it.
Adapter Pattern
Converts the interface of a class into another interface clients expect, enabling incompatible interfaces to work together.
- ✓Adapter converts an existing interface into one a client expects — "make it fit".
- ✓Object Adapter (composition) is preferred in Java over Class Adapter (inheritance).
- ✓Adapter pattern is used when integrating third-party libraries without modifying them.
- ✓Arrays.asList(), InputStreamReader, and Collections.enumeration() are canonical Java Adapter examples.
- ✓Adapter and Decorator both wrap objects — Adapter changes the interface; Decorator keeps it the same.
// Target interface — what the client expects
public interface PaymentGateway {
boolean charge(String userId, double amountInRupees);
boolean refund(String transactionId);
}
// Adaptee — third-party Stripe SDK (incompatible interface, cannot be changed)
public class StripeSdk {
public StripeResponse createCharge(StripeChargeRequest request) {
System.out.println("Stripe: charging " + request.getAmountInCents() + " cents");
return new StripeResponse("ch_123", true);
}
public StripeResponse reverseCharge(String chargeId) {
System.out.println("Stripe: reversing charge " + chargeId);
return new StripeResponse(chargeId, true);
}
}
// Supporting Stripe DTOs
class StripeChargeRequest {
private long amountInCents;
private String currency;
public StripeChargeRequest(long amountInCents, String currency) {
this.amountInCents = amountInCents;
this.currency = currency;
}
public long getAmountInCents() { return amountInCents; }
}
class StripeResponse {
private final String chargeId;
private final boolean success;
public StripeResponse(String chargeId, boolean success) {
this.chargeId = chargeId; this.success = success;
}
public boolean isSuccess() { return success; }
public String getChargeId() { return chargeId; }
}
// Adapter — wraps Stripe SDK, implements our PaymentGateway interface
public class StripePaymentAdapter implements PaymentGateway {
private final StripeSdk stripe; // composition — holds the adaptee
public StripePaymentAdapter(StripeSdk stripe) {
this.stripe = stripe;
}
@Override
public boolean charge(String userId, double amountInRupees) {
// Translate: rupees → paise (INR smallest unit), double → long
long amountInPaise = Math.round(amountInRupees * 100);
StripeChargeRequest request = new StripeChargeRequest(amountInPaise, "INR");
StripeResponse response = stripe.createCharge(request);
return response.isSuccess();
}
@Override
public boolean refund(String transactionId) {
StripeResponse response = stripe.reverseCharge(transactionId);
return response.isSuccess();
}
}
// Client code — depends only on PaymentGateway, unaware of Stripe
public class CheckoutService {
private final PaymentGateway gateway;
public CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
public void checkout(String userId, double amount) {
boolean success = gateway.charge(userId, amount);
System.out.println("Payment " + (success ? "succeeded" : "failed"));
}
}
// Wiring
CheckoutService service = new CheckoutService(new StripePaymentAdapter(new StripeSdk()));
service.checkout("user-42", 999.0);Decorator Pattern
Attaches additional responsibilities to an object dynamically at runtime by wrapping it, as a flexible alternative to subclassing.
- ✓Decorator and Adapter both wrap objects — Decorator keeps the same interface; Adapter changes it.
- ✓Decorators are stacked at runtime — the order matters (TrimDecorator before HtmlEscapeDecorator avoids escaping spaces).
- ✓Java I/O streams (BufferedInputStream, DataInputStream) are the canonical Decorator example.
- ✓Prefer Decorator over inheritance when combining features leads to a class explosion.
- ✓A Decorator does not know which concrete class it wraps — it depends on the Component interface.
// Component interface
public interface TextProcessor {
String process(String text);
}
// Concrete Component — base implementation
public class PlainTextProcessor implements TextProcessor {
@Override
public String process(String text) {
return text; // just return as-is
}
}
// Abstract Decorator — holds a reference to another TextProcessor
public abstract class TextProcessorDecorator implements TextProcessor {
protected final TextProcessor wrapped;
public TextProcessorDecorator(TextProcessor wrapped) {
this.wrapped = Objects.requireNonNull(wrapped);
}
}
// Concrete Decorator 1 — HTML escape
public class HtmlEscapeDecorator extends TextProcessorDecorator {
public HtmlEscapeDecorator(TextProcessor wrapped) { super(wrapped); }
@Override
public String process(String text) {
String processed = wrapped.process(text); // delegate first
return processed
.replace("&", "&")
.replace("<", "<")
.replace(">", ">");
}
}
// Concrete Decorator 2 — Trim whitespace
public class TrimDecorator extends TextProcessorDecorator {
public TrimDecorator(TextProcessor wrapped) { super(wrapped); }
@Override
public String process(String text) {
return wrapped.process(text.trim()); // trim before delegating
}
}
// Concrete Decorator 3 — Uppercase
public class UpperCaseDecorator extends TextProcessorDecorator {
public UpperCaseDecorator(TextProcessor wrapped) { super(wrapped); }
@Override
public String process(String text) {
return wrapped.process(text).toUpperCase(); // uppercase after delegating
}
}
// Stacking decorators at runtime
TextProcessor processor = new UpperCaseDecorator(
new HtmlEscapeDecorator(
new TrimDecorator(
new PlainTextProcessor())));
String result = processor.process(" <hello world> ");
System.out.println(result); // <HELLO WORLD>Facade Pattern
Provides a simplified interface to a complex subsystem, hiding its internal complexity from clients.
- ✓Facade simplifies complex subsystems — does not prevent direct access to subsystem classes.
- ✓In Spring, the @Service layer is a Facade over repositories, clients, and event publishers.
- ✓Facade reduces coupling between clients and subsystem internals (changes in subsystem do not affect clients).
- ✓Unlike Adapter, Facade wraps a whole subsystem, not one incompatible class.
- ✓JdbcTemplate is a Facade over raw JDBC: it hides connection management, statement creation, and result set iteration.
// Complex subsystem classes
public class InventoryService {
public boolean reserveStock(String productId, int qty) {
System.out.println("Reserving " + qty + " units of " + productId);
return true;
}
public void releaseReservation(String productId, int qty) {
System.out.println("Releasing reservation for " + productId);
}
}
public class PaymentService {
public String processPayment(String userId, double amount) {
System.out.println("Processing payment of " + amount + " for " + userId);
return "txn-" + System.currentTimeMillis();
}
public void refund(String transactionId) {
System.out.println("Refunding transaction " + transactionId);
}
}
public class ShippingService {
public String createShipment(String orderId, String address) {
System.out.println("Creating shipment for order " + orderId + " to " + address);
return "ship-" + orderId;
}
}
public class NotificationService {
public void sendOrderConfirmation(String userId, String orderId) {
System.out.println("Sending confirmation to user " + userId + " for order " + orderId);
}
}
// Facade — simplifies the multi-step order placement process
public class OrderFacade {
private final InventoryService inventory;
private final PaymentService payment;
private final ShippingService shipping;
private final NotificationService notifier;
public OrderFacade(InventoryService inventory, PaymentService payment,
ShippingService shipping, NotificationService notifier) {
this.inventory = inventory;
this.payment = payment;
this.shipping = shipping;
this.notifier = notifier;
}
// One-call interface hiding the 4-step orchestration
public String placeOrder(String userId, String productId,
int qty, double amount, String address) {
String orderId = "ORD-" + System.currentTimeMillis();
if (!inventory.reserveStock(productId, qty)) {
throw new IllegalStateException("Out of stock: " + productId);
}
String txnId;
try {
txnId = payment.processPayment(userId, amount);
} catch (RuntimeException e) {
inventory.releaseReservation(productId, qty);
throw e;
}
shipping.createShipment(orderId, address);
notifier.sendOrderConfirmation(userId, orderId);
return orderId;
}
}
// Client — one line instead of coordinating four subsystems
OrderFacade facade = new OrderFacade(
new InventoryService(), new PaymentService(),
new ShippingService(), new NotificationService());
String orderId = facade.placeOrder("user-1", "LLD-BOOK", 1, 499.0, "Pune, India");Proxy Pattern
Provides a surrogate or placeholder for another object to control access, add caching, logging, or lazy initialization.
- ✓Four proxy types: Virtual (lazy init), Protection (access control), Remote (network), Cache (memoization).
- ✓Proxy and Decorator look identical in code — intent differs: Proxy controls access; Decorator adds behavior.
- ✓Spring @Transactional and @Cacheable use dynamic proxies — self-invocation bypasses them.
- ✓JDK dynamic proxies require an interface; CGLIB proxies subclass the target (no interface needed).
- ✓Proxy is transparent to the client — client cannot tell it is talking to a proxy.
// Subject interface
public interface ImageLoader {
byte[] loadImage(String imageId);
}
// Real Subject — expensive (hits S3)
public class S3ImageLoader implements ImageLoader {
@Override
public byte[] loadImage(String imageId) {
System.out.println("Fetching from S3: " + imageId); // slow network call
return new byte[]{1, 2, 3}; // simulated image data
}
}
// Cache Proxy — wraps real loader, caches results
public class CachedImageLoader implements ImageLoader {
private final ImageLoader delegate;
private final Map<String, byte[]> cache = new ConcurrentHashMap<>();
public CachedImageLoader(ImageLoader delegate) {
this.delegate = delegate;
}
@Override
public byte[] loadImage(String imageId) {
return cache.computeIfAbsent(imageId, id -> {
System.out.println("Cache MISS for: " + id);
return delegate.loadImage(id);
});
}
}
// Protection Proxy — checks permissions before delegating
public class SecureImageLoader implements ImageLoader {
private final ImageLoader delegate;
private final SecurityContext security;
public SecureImageLoader(ImageLoader delegate, SecurityContext security) {
this.delegate = delegate;
this.security = security;
}
@Override
public byte[] loadImage(String imageId) {
if (!security.hasPermission("IMAGE_READ")) {
throw new AccessDeniedException("No permission to read image: " + imageId);
}
return delegate.loadImage(imageId);
}
}
// Stacking proxies (Protection → Cache → Real)
ImageLoader loader = new SecureImageLoader(
new CachedImageLoader(
new S3ImageLoader()), securityCtx);
loader.loadImage("course-thumbnail.jpg"); // checks permission, then cache, then S3Composite Pattern
Composes objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions uniformly.
- ✓Composite lets clients treat Leaf and Composite nodes uniformly through the Component interface.
- ✓The Composite holds a list of Component children — each may be a Leaf or another Composite (recursion).
- ✓size(), evaluate(), render() methods are naturally recursive in Composite structures.
- ✓The Component interface should not expose child management (add/remove) — that belongs only on Composite.
- ✓Real-world: javax.swing.JComponent (UI tree), XML DOM, org charts, JSON/YAML object trees.
import java.util.ArrayList;
import java.util.List;
// Component interface
public interface FileSystemEntry {
String getName();
long size(); // recursive for directories
void print(String indent);
}
// Leaf — has no children
public class File implements FileSystemEntry {
private final String name;
private final long sizeBytes;
public File(String name, long sizeBytes) {
this.name = name;
this.sizeBytes = sizeBytes;
}
@Override public String getName() { return name; }
@Override public long size() { return sizeBytes; }
@Override
public void print(String indent) {
System.out.println(indent + "📄 " + name + " (" + sizeBytes + " bytes)");
}
}
// Composite — contains children (Files or Directories)
public class Directory implements FileSystemEntry {
private final String name;
private final List<FileSystemEntry> children = new ArrayList<>();
public Directory(String name) { this.name = name; }
public void add(FileSystemEntry entry) { children.add(entry); }
public void remove(FileSystemEntry entry) { children.remove(entry); }
@Override public String getName() { return name; }
@Override
public long size() {
return children.stream()
.mapToLong(FileSystemEntry::size) // recursive
.sum();
}
@Override
public void print(String indent) {
System.out.println(indent + "📁 " + name + "/ (" + size() + " bytes)");
children.forEach(child -> child.print(indent + " "));
}
}
// Building the tree
Directory root = new Directory("root");
Directory src = new Directory("src");
src.add(new File("Main.java", 1024));
src.add(new File("Config.java", 512));
Directory resources = new Directory("resources");
resources.add(new File("application.yml", 256));
root.add(src);
root.add(resources);
root.add(new File("README.md", 128));
root.print("");
System.out.println("Total size: " + root.size() + " bytes"); // 1920Bridge Pattern
Decouples an abstraction from its implementation so that the two can vary independently.
- ✓Bridge prevents M×N class explosion when two independent dimensions vary.
- ✓The abstraction holds a reference to the implementor — both sides can vary independently.
- ✓Bridge differs from Strategy: Bridge is structural (design-time hierarchy split); Strategy is behavioral (runtime algorithm swap).
- ✓Use Bridge when both the abstraction AND implementation need subclassing independently.
- ✓JDBC is a Bridge: Java application (abstraction) calls DriverManager/Connection API; JDBC driver (implementor) implements for each DB vendor.
// Implementor interface
public interface Renderer {
void renderCircle(double radius);
void renderSquare(double side);
}
// Concrete Implementors
public class VectorRenderer implements Renderer {
@Override
public void renderCircle(double radius) {
System.out.printf("Drawing VECTOR circle with radius %.1f%n", radius);
}
@Override
public void renderSquare(double side) {
System.out.printf("Drawing VECTOR square with side %.1f%n", side);
}
}
public class RasterRenderer implements Renderer {
@Override
public void renderCircle(double radius) {
System.out.printf("Drawing RASTER circle (pixels) radius %.1f%n", radius);
}
@Override
public void renderSquare(double side) {
System.out.printf("Drawing RASTER square (pixels) side %.1f%n", side);
}
}
// Abstraction — holds reference to Implementor (the bridge)
public abstract class Shape {
protected final Renderer renderer; // bridge to implementation
protected Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
public abstract void resize(double factor);
}
// Refined Abstractions
public class Circle extends Shape {
private double radius;
public Circle(Renderer renderer, double radius) {
super(renderer);
this.radius = radius;
}
@Override public void draw() { renderer.renderCircle(radius); }
@Override public void resize(double factor) { radius *= factor; }
}
public class Square extends Shape {
private double side;
public Square(Renderer renderer, double side) {
super(renderer);
this.side = side;
}
@Override public void draw() { renderer.renderSquare(side); }
@Override public void resize(double factor) { side *= factor; }
}
// Combining dimensions independently
Shape vectorCircle = new Circle(new VectorRenderer(), 5.0);
Shape rasterSquare = new Square(new RasterRenderer(), 3.0);
vectorCircle.draw(); // Drawing VECTOR circle with radius 5.0
rasterSquare.draw(); // Drawing RASTER square (pixels) side 3.0
// Switch renderer at runtime
Shape adaptedCircle = new Circle(new RasterRenderer(), 5.0);
adaptedCircle.draw(); // Drawing RASTER circle (pixels) radius 5.0Flyweight Pattern
Uses sharing to support large numbers of fine-grained objects efficiently by separating intrinsic (shared) state from extrinsic (context-specific) state.
- ✓Intrinsic state is shared and immutable — stored in the Flyweight.
- ✓Extrinsic state is context-dependent — passed as parameters at call time.
- ✓FlyweightFactory caches instances by key — computeIfAbsent() for thread-safe lazy creation.
- ✓Java String pool and Integer.valueOf(-128..127) cache are canonical Flyweight examples.
- ✓Flyweight reduces memory; it increases code complexity — only use when profiling proves memory pressure.
import java.util.HashMap;
import java.util.Map;
// Flyweight — stores INTRINSIC state only (shared, immutable)
public final class CharacterGlyph {
private final char character; // intrinsic: the glyph shape
private final String fontFamily; // intrinsic: font (shared per char+font combo)
private final int fontSize;
public CharacterGlyph(char character, String fontFamily, int fontSize) {
this.character = character;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
System.out.println("Creating new glyph for: '" + character + "' " + fontFamily);
}
// Extrinsic state (position, color) passed at render time
public void render(int x, int y, String color) {
System.out.printf("Rendering '%c' at (%d,%d) color=%s font=%s%n",
character, x, y, color, fontFamily);
}
}
// FlyweightFactory — cache shared instances
public class GlyphFactory {
private static final Map<String, CharacterGlyph> CACHE = new HashMap<>();
public static CharacterGlyph getGlyph(char c, String font, int size) {
String key = c + "-" + font + "-" + size;
return CACHE.computeIfAbsent(key, k -> new CharacterGlyph(c, font, size));
}
public static int cachedCount() { return CACHE.size(); }
}
// Context — holds extrinsic state + reference to shared Flyweight
public class CharacterContext {
private final CharacterGlyph glyph; // shared flyweight
private final int x, y; // extrinsic: position
private final String color; // extrinsic: color
public CharacterContext(char c, String font, int size, int x, int y, String color) {
this.glyph = GlyphFactory.getGlyph(c, font, size); // shared instance
this.x = x; this.y = y; this.color = color;
}
public void render() { glyph.render(x, y, color); }
}
// Rendering a document with 1000 'A' characters — only ONE CharacterGlyph created
List<CharacterContext> document = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
document.add(new CharacterContext('A', "Arial", 12, i * 10, 0, "black"));
}
document.forEach(CharacterContext::render);
System.out.println("Glyphs in cache: " + GlyphFactory.cachedCount()); // 1, not 1000Filter / Chain of Responsibility Pattern
Passes a request along a chain of handlers where each handler either processes it, enriches it, or forwards it to the next handler.
- ✓Chain of Responsibility decouples sender from receiver — sender does not know which handler processes the request.
- ✓Each handler can process, enrich, forward, or short-circuit the request.
- ✓Servlet filters and Spring Security filter chain are production examples of this pattern.
- ✓Filter order matters — authentication before authorization, logging wrapping both.
- ✓Unlike Command pattern, Chain of Responsibility has multiple potential handlers; Command has one.
// Abstract Handler
public abstract class ApprovalHandler {
protected ApprovalHandler next;
public ApprovalHandler setNext(ApprovalHandler next) {
this.next = next;
return next; // fluent chaining
}
public abstract void handleRequest(ExpenseRequest request);
}
public class ExpenseRequest {
public final double amount;
public final String description;
public ExpenseRequest(double amount, String description) {
this.amount = amount; this.description = description;
}
}
// Concrete Handlers
public class TeamLeadApprover extends ApprovalHandler {
@Override
public void handleRequest(ExpenseRequest request) {
if (request.amount <= 1_000) {
System.out.println("Team Lead approved: " + request.description);
} else if (next != null) {
next.handleRequest(request); // forward up the chain
}
}
}
public class ManagerApprover extends ApprovalHandler {
@Override
public void handleRequest(ExpenseRequest request) {
if (request.amount <= 10_000) {
System.out.println("Manager approved: " + request.description);
} else if (next != null) {
next.handleRequest(request);
}
}
}
public class DirectorApprover extends ApprovalHandler {
@Override
public void handleRequest(ExpenseRequest request) {
System.out.println("Director approved: " + request.description + " (₹" + request.amount + ")");
}
}
// Build the chain
ApprovalHandler chain = new TeamLeadApprover();
chain.setNext(new ManagerApprover())
.setNext(new DirectorApprover());
chain.handleRequest(new ExpenseRequest(500, "Team lunch")); // Team Lead
chain.handleRequest(new ExpenseRequest(5_000, "Laptop RAM")); // Manager
chain.handleRequest(new ExpenseRequest(50_000, "Server upgrade")); // Director