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 instance4
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.007
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