Cheat SheetsJava A–ZDesign Patterns

Design Patterns — Cheat Sheet

Java A–Z · 10 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Design Patterns
Java A–Z10 topicsQuick revision reference
1

Builder Pattern

  • Builder solves the telescoping constructor problem when a class has many optional fields.
  • Each setter returns this (the Builder) for fluent method chaining.
  • Make the target constructor private — only the Builder should create instances.
  • Lombok @Builder generates the entire builder at compile time.
  • @Builder.Default sets field defaults; @Singular adds single-element add methods for collections.
DatabaseConfig.java
public final class DatabaseConfig {
    private final String host;
    private final int port;
    private final String database;
    private final int maxConnections;
    private final Duration timeout;
    private final boolean ssl;

    private DatabaseConfig(Builder b) {
        this.host           = b.host;
        this.port           = b.port;
        this.database       = b.database;
        this.maxConnections = b.maxConnections;
        this.timeout        = b.timeout;
        this.ssl            = b.ssl;
    }

    public static Builder builder() { return new Builder(); }

    public static class Builder {
        private String   host           = "localhost";
        private int      port           = 5432;
        private String   database;
        private int      maxConnections = 10;
        private Duration timeout        = Duration.ofSeconds(30);
        private boolean  ssl            = false;

        public Builder host(String host)       { this.host = host; return this; }
        public Builder port(int port)          { this.port = port; return this; }
        public Builder database(String db)     { this.database = db; return this; }
        public Builder maxConnections(int n)   { this.maxConnections = n; return this; }
        public Builder timeout(Duration t)     { this.timeout = t; return this; }
        public Builder ssl(boolean ssl)        { this.ssl = ssl; return this; }

        public DatabaseConfig build() {
            if (database == null) throw new IllegalStateException("database required");
            return new DatabaseConfig(this);
        }
    }
}

// Usage
DatabaseConfig config = DatabaseConfig.builder()
    .host("db.example.com")
    .database("production")
    .maxConnections(50)
    .ssl(true)
    .build();
2

Singleton Pattern

  • Enum singleton is the most robust: thread-safe, serialisation-safe, reflection-safe.
  • Double-checked locking requires volatile — without it, partially-constructed instances can be observed.
  • Initialization-on-demand holder pattern provides thread-safe lazy loading elegantly.
  • All Spring beans are singletons by default — implement the pattern manually only outside Spring.
  • Singleton is often considered an anti-pattern in testing because it introduces global state.
SingletonImpls.java
// 1. Eager initialization (simple, safe)
public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    private EagerSingleton() {}
    public static EagerSingleton getInstance() { return INSTANCE; }
}

// 2. Double-checked locking (lazy + fast)
public class DCLSingleton {
    private static volatile DCLSingleton instance;
    private DCLSingleton() {}
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {         // second check
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

// 3. Initialization-on-demand holder (preferred lazy)
public class HolderSingleton {
    private HolderSingleton() {}
    private static class Holder {
        static final HolderSingleton INSTANCE = new HolderSingleton();
    }
    public static HolderSingleton getInstance() {
        return Holder.INSTANCE; // class loaded lazily
    }
}
3

Factory Pattern

  • Static factory methods have names, can cache, and can return subtypes — prefer over constructors for complex creation.
  • Factory Method delegates instantiation to subclasses — open/closed principle in action.
  • Abstract Factory creates consistent families of related objects without coupling to concrete classes.
  • Java standard library uses static factories extensively: List.of(), Optional.of(), Path.of().
  • Factory patterns improve testability — inject a mock factory to control what is created.
StaticFactory.java
public class Currency {
    private final String code;
    private static final Map<String, Currency> CACHE = new HashMap<>();

    private Currency(String code) { this.code = code; }

    // Static factory methods — named, can cache
    public static Currency of(String code) {
        return CACHE.computeIfAbsent(
            code.toUpperCase(), Currency::new);
    }

    public static Currency usd() { return of("USD"); }
    public static Currency eur() { return of("EUR"); }

    // vs constructors — can return subtype
    public static Number parse(String s) {
        if (s.contains(".")) return Double.parseDouble(s);
        return Long.parseLong(s);
    }
}

// Client — no coupling to Currency constructor
Currency usd  = Currency.of("USD");
Currency usd2 = Currency.of("USD");
assert usd == usd2; // same cached instance
4

Observer Pattern

  • Observer decouples subjects from observers — neither knows the concrete type of the other.
  • Subject holds a list of Observer references; calls update() on each when state changes.
  • Java standard library: PropertyChangeSupport, Swing listeners, JavaFX properties.
  • Spring: ApplicationEventPublisher + @EventListener is the idiomatic Observer.
  • Be careful of memory leaks — unregister observers when no longer needed.
Observer.java
import java.util.*;

// Observer interface
public interface StockObserver {
    void onPriceChange(String symbol, double newPrice);
}

// Subject
public class StockTicker {
    private final Map<String, Double> prices = new HashMap<>();
    private final List<StockObserver> observers = new ArrayList<>();

    public void subscribe(StockObserver observer) {
        observers.add(observer);
    }
    public void unsubscribe(StockObserver observer) {
        observers.remove(observer);
    }

    public void updatePrice(String symbol, double price) {
        prices.put(symbol, price);
        notifyObservers(symbol, price);
    }

    private void notifyObservers(String symbol, double price) {
        for (StockObserver observer : observers) {
            observer.onPriceChange(symbol, price);
        }
    }
}

// Concrete observers
StockTicker ticker = new StockTicker();
ticker.subscribe((sym, price) ->
    System.out.printf("Alert: %s hit %.2f%n", sym, price));
ticker.subscribe((sym, price) ->
    System.out.printf("Log:   %s = %.2f%n", sym, price));

ticker.updatePrice("AAPL", 185.50);
5

Strategy Pattern

  • Strategy encapsulates algorithms behind a common interface, eliminating if-else/switch.
  • The context holds a strategy reference and delegates; it does not know the concrete type.
  • In Java 8+, functional interfaces + lambdas make Strategy extremely lightweight.
  • Comparator<T> is the most-used Strategy in the JDK.
  • Strategy enables runtime algorithm selection and satisfies the Open/Closed Principle.
Strategy.java
// Strategy interface
public interface SortStrategy {
    void sort(int[] array);
}

// Concrete strategies
public class QuickSort implements SortStrategy {
    @Override
    public void sort(int[] array) { /* quicksort impl */ }
}

public class MergeSort implements SortStrategy {
    @Override
    public void sort(int[] array) { /* mergesort impl */ }
}

// Context
public class DataProcessor {
    private SortStrategy strategy;

    public DataProcessor(SortStrategy strategy) {
        this.strategy = strategy;
    }

    // Switch strategy at runtime
    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;
    }

    public void process(int[] data) {
        strategy.sort(data);
        // further processing...
    }
}

// Client
DataProcessor processor = new DataProcessor(new QuickSort());
processor.process(data);

// Switch strategy based on data size
if (data.length > 10_000) {
    processor.setStrategy(new MergeSort());
}
6

Decorator Pattern

  • Decorator wraps a component implementing the same interface, adding behaviour via delegation.
  • Decorators can be stacked in any combination — this is more flexible than inheritance.
  • Java I/O streams (BufferedInputStream, GZIPInputStream, DataInputStream) are the classic JDK example.
  • Functional Decorator = higher-order function that wraps another function.
  • The key difference from inheritance: Decorator adds behaviour at runtime, not compile time.
CoffeeDecorator.java
// Component interface
public interface Coffee {
    String getDescription();
    double getCost();
}

// Concrete component
public class SimpleCoffee implements Coffee {
    @Override public String getDescription() { return "Coffee"; }
    @Override public double getCost()        { return 1.00; }
}

// Abstract decorator
public abstract class CoffeeDecorator implements Coffee {
    protected final Coffee wrapped;
    public CoffeeDecorator(Coffee coffee) { this.wrapped = coffee; }
    @Override public String getDescription() { return wrapped.getDescription(); }
    @Override public double getCost()        { return wrapped.getCost(); }
}

// Concrete decorators
public class Milk extends CoffeeDecorator {
    public Milk(Coffee c) { super(c); }
    @Override public String getDescription() { return wrapped.getDescription() + ", Milk"; }
    @Override public double getCost()        { return wrapped.getCost() + 0.25; }
}

public class Vanilla extends CoffeeDecorator {
    public Vanilla(Coffee c) { super(c); }
    @Override public String getDescription() { return wrapped.getDescription() + ", Vanilla"; }
    @Override public double getCost()        { return wrapped.getCost() + 0.50; }
}

// Stack decorators at runtime
Coffee order = new Vanilla(new Milk(new Milk(new SimpleCoffee())));
System.out.println(order.getDescription()); // Coffee, Milk, Milk, Vanilla
System.out.println(order.getCost());        // 2.00
7

Adapter Pattern

  • Adapter converts one interface to another — it does not add behaviour, only translates.
  • Prefer object adapter (composition) over class adapter (inheritance) for flexibility.
  • Arrays.asList, InputStreamReader, and Collections.enumeration are JDK Adapters.
  • Adapter bridges incompatibilities; Decorator enhances; Facade simplifies.
  • The client depends on the target interface, not on the adaptee — good dependency inversion.
PaymentAdapter.java
// Target interface (what the client expects)
public interface PaymentGateway {
    PaymentResult charge(String customerId, double amount, String currency);
}

// Existing class with incompatible interface (adaptee)
public class LegacyPaymentSystem {
    public String processPayment(int custId, long amountCents) {
        // old implementation
        return "TXN-" + custId + "-" + amountCents;
    }
}

// Adapter — wraps LegacyPaymentSystem, implements PaymentGateway
public class LegacyPaymentAdapter implements PaymentGateway {
    private final LegacyPaymentSystem legacy;

    public LegacyPaymentAdapter(LegacyPaymentSystem legacy) {
        this.legacy = legacy;
    }

    @Override
    public PaymentResult charge(String customerId, double amount, String currency) {
        // Translate: String → int, double → long cents
        int custId      = Integer.parseInt(customerId);
        long amountCents = Math.round(amount * 100);
        String txnId    = legacy.processPayment(custId, amountCents);
        return new PaymentResult(txnId, "SUCCESS");
    }
}

// Client — uses target interface, unaware of legacy system
PaymentGateway gateway = new LegacyPaymentAdapter(new LegacyPaymentSystem());
PaymentResult result = gateway.charge("12345", 99.99, "USD");
8

Template Method Pattern

  • Template method is final; subclasses implement abstract steps and optionally override hooks.
  • The Hollywood Principle: the base class calls subclass methods, not the reverse.
  • AbstractList, HttpServlet, and InputStream use Template Method in the JDK.
  • Hook methods are optional steps with default (often no-op) implementations.
  • Template Method = inheritance-based variation; Strategy = composition-based — prefer Strategy for flexibility.
TemplateMethod.java
// Abstract class with template method
public abstract class DataExporter {

    // Template method — defines the algorithm skeleton
    public final void export(String destination) {
        List<Object> data = fetchData();
        List<Object> validated = validate(data);
        String formatted = format(validated);
        write(formatted, destination);
        if (shouldNotify()) {           // hook method
            sendNotification(destination);
        }
    }

    // Steps subclasses must implement
    protected abstract List<Object> fetchData();
    protected abstract String format(List<Object> data);

    // Step with default implementation
    protected List<Object> validate(List<Object> data) {
        return data.stream()
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }

    // Hook method — optional override
    protected boolean shouldNotify() { return false; }

    private void write(String data, String dest) {
        Files.writeString(Path.of(dest), data);
    }

    private void sendNotification(String dest) {
        System.out.println("Export complete: " + dest);
    }
}

// Subclass — implements specific steps
public class CsvExporter extends DataExporter {
    @Override
    protected List<Object> fetchData() { return userRepository.findAll(); }

    @Override
    protected String format(List<Object> data) {
        return data.stream().map(Object::toString)
            .collect(Collectors.joining("\n"));
    }

    @Override
    protected boolean shouldNotify() { return true; } // override hook
}
9

Command Pattern

  • Command encapsulates a request as an object — decouples invoker from receiver.
  • Storing commands enables undo/redo, queuing, logging, and event sourcing.
  • MacroCommand composes multiple commands; undo reverses them in reverse order.
  • Runnable and Callable are the JDK's built-in Commands for fire-and-forget tasks.
  • Use full Command pattern when you need history/undo; use lambda for simple cases.
TextEditorCommand.java
// Command interface
public interface Command {
    void execute();
    void undo();  // optional — enables undo/redo
}

// Receiver — the object that actually does the work
public class TextEditor {
    private final StringBuilder text = new StringBuilder();
    public void insertText(String s) { text.append(s); }
    public void deleteText(int len)  { text.delete(text.length() - len, text.length()); }
    public String getText()          { return text.toString(); }
}

// Concrete Command
public class InsertCommand implements Command {
    private final TextEditor editor;
    private final String text;

    public InsertCommand(TextEditor editor, String text) {
        this.editor = editor;
        this.text   = text;
    }

    @Override public void execute() { editor.insertText(text); }
    @Override public void undo()    { editor.deleteText(text.length()); }
}

// Invoker — triggers commands, maintains history for undo
public class CommandHistory {
    private final Deque<Command> history = new ArrayDeque<>();

    public void execute(Command cmd) {
        cmd.execute();
        history.push(cmd);
    }

    public void undo() {
        if (!history.isEmpty()) history.pop().undo();
    }
}
10

Composite Pattern

  • Composite allows clients to treat Leaf and Composite objects uniformly via a Component interface.
  • Composite delegates operations to its children recursively.
  • Swing's Container/Component hierarchy is the classic JDK example of Composite.
  • Operations defined on the Component interface automatically work for the entire tree.
  • Use Composite when you have part-whole hierarchies (file systems, UI trees, expression trees).
FileSystem.java
// Component interface
public interface FileSystemItem {
    String name();
    long size();
    void print(String indent);
}

// Leaf
public record File(String name, long size) implements FileSystemItem {
    @Override public void print(String indent) {
        System.out.printf("%s📄 %s (%,d bytes)%n", indent, name, size);
    }
}

// Composite
public class Directory implements FileSystemItem {
    private final String name;
    private final List<FileSystemItem> children = new ArrayList<>();

    public Directory(String name) { this.name = name; }

    public void add(FileSystemItem item)    { children.add(item); }
    public void remove(FileSystemItem item) { children.remove(item); }

    @Override public String name() { return name; }

    @Override public long size() {
        return children.stream().mapToLong(FileSystemItem::size).sum();
    }

    @Override public void print(String indent) {
        System.out.printf("%s📁 %s (%,d bytes)%n", indent, name, size());
        children.forEach(c -> c.print(indent + "  "));
    }
}
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/java