Cheat SheetsLow Level DesignStructural Patterns

Structural Patterns — Cheat Sheet

Low Level Design · 8 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Structural Patterns
Low Level Design8 topicsQuick revision reference
1

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.
Java — Object Adapter (Stripe SDK to PaymentGateway)
// 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);
2

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.
Java — Stacked Decorators (TextProcessor)
// 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("<", "&lt;")
            .replace(">", "&gt;");
    }
}

// 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); // &LT;HELLO WORLD&GT;
3

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.
Java — OrderFacade over four subsystem services
// 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");
4

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.
Java — Cache Proxy and Protection 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 S3
5

Composite 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.
Java — Composite File System (File + Directory)
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"); // 1920
6

Bridge 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.
Java — Bridge (Shape × Renderer)
// 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.0
7

Flyweight 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.
Java — Flyweight (CharacterGlyph + GlyphFactory)
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 1000
8

Filter / 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.
Java — Chain of Responsibility (Expense Approval)
// 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
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/lld