Java Design Patterns Cheat Sheet
Shows implementations of core Gang of Four design patterns including Singleton, Factory Method, Builder, and Observer in Java.
Singleton
Ensure a class has only one instance with global access.
public class ConfigManager { private static volatile ConfigManager instance; private ConfigManager() {} public static ConfigManager getInstance() { if (instance == null) { synchronized (ConfigManager.class) { if (instance == null) { instance = new ConfigManager(); // double-checked locking } } } return instance; }}// Simplest thread-safe form: enum singletonpublic enum Config { INSTANCE; }
Factory Method
Delegate object creation to subclasses via a common interface.
interface Notification { void send(String message);}class EmailNotification implements Notification { public void send(String message) { System.out.println("Email: " + message); }}class SmsNotification implements Notification { public void send(String message) { System.out.println("SMS: " + message); }}class NotificationFactory { static Notification create(String type) { return switch (type) { case "EMAIL" -> new EmailNotification(); case "SMS" -> new SmsNotification(); default -> throw new IllegalArgumentException("Unknown type: " + type); }; }}
Builder
Construct complex objects step by step with a fluent API.
public class Pizza { private final String size; private final boolean cheese; private final boolean pepperoni; private Pizza(Builder b) { this.size = b.size; this.cheese = b.cheese; this.pepperoni = b.pepperoni; } public static class Builder { private String size = "medium"; private boolean cheese; private boolean pepperoni; public Builder size(String size) { this.size = size; return this; } public Builder cheese(boolean v) { this.cheese = v; return this; } public Builder pepperoni(boolean v) { this.pepperoni = v; return this; } public Pizza build() { return new Pizza(this); } }}Pizza pizza = new Pizza.Builder().size("large").cheese(true).build();
Observer
Notify multiple dependents automatically when subject state changes.
interface Observer { void update(String event); }class EventBus { private final List<Observer> observers = new ArrayList<>(); void subscribe(Observer o) { observers.add(o); } void unsubscribe(Observer o) { observers.remove(o); } void publish(String event) { for (Observer o : observers) o.update(event); }}EventBus bus = new EventBus();bus.subscribe(event -> System.out.println("Received: " + event));bus.publish("user.created");
Common Patterns at a Glance
Quick reference to widely used GoF patterns beyond the ones shown above.
- Singleton- Ensures a class has only one instance and provides a global access point to it.
- Factory Method- Lets subclasses decide which concrete class to instantiate behind a common interface.
- Builder- Separates complex object construction from its representation using a fluent, step-by-step API.
- Observer- Defines a one-to-many dependency so observers are notified automatically when a subject changes.
- Strategy- Encapsulates interchangeable algorithms behind a common interface, selectable at runtime.
- Adapter- Converts one interface into another that a client expects, without modifying either.
- Decorator- Adds responsibilities to an object dynamically by wrapping it, as an alternative to subclassing.
- Command- Encapsulates a request as an object, enabling queuing, logging, and undoable operations.
Strategy with Lambdas
Replace verbose strategy subclasses with functional interfaces for lightweight, swappable algorithms.
@FunctionalInterfaceinterface DiscountStrategy { BigDecimal apply(BigDecimal price);}class CheckoutService { private final DiscountStrategy strategy; CheckoutService(DiscountStrategy strategy) { this.strategy = strategy; } BigDecimal total(BigDecimal price) { return strategy.apply(price); }}DiscountStrategy tenPercentOff = price -> price.multiply(BigDecimal.valueOf(0.9));DiscountStrategy noDiscount = price -> price;new CheckoutService(tenPercentOff).total(BigDecimal.valueOf(100)); // 90.0
Decorator
Layer behavior onto an object at runtime without modifying its class or using inheritance chains.
interface Coffee { double cost(); String description();}class Espresso implements Coffee { public double cost() { return 2.0; } public String description() { return "Espresso"; }}abstract class CoffeeDecorator implements Coffee { protected final Coffee wrapped; CoffeeDecorator(Coffee wrapped) { this.wrapped = wrapped; }}class WithMilk extends CoffeeDecorator { WithMilk(Coffee c) { super(c); } public double cost() { return wrapped.cost() + 0.5; } public String description() { return wrapped.description() + " + milk"; }}Coffee order = new WithMilk(new Espresso());System.out.println(order.description() + " = " + order.cost()); // Espresso + milk = 2.5
Adapter
Bridge an incompatible legacy interface to the one your client code expects, without touching either side.
// Legacy third-party API you can't changeclass LegacyXmlLogger { void writeXmlEntry(String xml) { /* ... */ }}// Interface your application code depends oninterface JsonLogger { void log(String jsonPayload);}class XmlLoggerAdapter implements JsonLogger { private final LegacyXmlLogger legacy; XmlLoggerAdapter(LegacyXmlLogger legacy) { this.legacy = legacy; } public void log(String jsonPayload) { String xml = convertJsonToXml(jsonPayload); legacy.writeXmlEntry(xml); } private String convertJsonToXml(String json) { return "<entry>" + json + "</entry>"; }}
Sealed Types as a Visitor Alternative
Modern Java replaces classic double-dispatch Visitor with sealed hierarchies plus exhaustive switch pattern matching.
sealed interface Shape permits Circle, Rectangle, Triangle {}record Circle(double radius) implements Shape {}record Rectangle(double w, double h) implements Shape {}record Triangle(double base, double height) implements Shape {}class AreaCalculator { static double area(Shape shape) { // Compiler enforces exhaustiveness - no default needed, no visitor boilerplate return switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.w() * r.h(); case Triangle t -> 0.5 * t.base() * t.height(); }; }}
Common Implementation Pitfalls
Mistakes teams make when applying GoF patterns literally in modern Java codebases.
- Singleton + DI container- Manually coding a Singleton class inside a Spring/CDI app fights the container; let the container scope the bean as a singleton instead.
- Builder overkill- Hand-rolled Builder for a 2-3 field class adds ceremony a simple constructor or record already handles.
- Observer memory leaks- Forgetting to unsubscribe long-lived observers from a long-lived subject keeps them reachable and unable to be garbage collected.
- Factory returning concrete types- A factory method that leaks the concrete implementation type in its return signature defeats the purpose of abstracting creation.
- Decorator interface bloat- Decorators must implement every method of the wrapped interface; a fat interface makes each decorator layer expensive to write.
- Double-checked locking without volatile- Skipping the volatile keyword on a lazily-initialized singleton field allows a partially constructed instance to be observed by another thread.
Favor composition-based patterns like Strategy over subclassing for interchangeable behavior - it lets you swap logic at runtime without an explosion of subclasses for every combination.