Design Patterns
Design patterns are reusable solutions to recurring design problems, named so teams can communicate intent quickly. The classic “Gang of Four” catalog groups twenty-three patterns into three categories. Patterns are tools, not goals — apply one when it removes real duplication or coupling, not to decorate simple code. The JDK itself is full of them, which makes it the best place to study them.
The Three Categories
| Category | Concern | Examples |
|---|---|---|
| Creational | how objects are created | Singleton, Factory, Builder, Abstract Factory, Prototype |
| Structural | how objects are composed | Adapter, Decorator, Proxy, Facade, Composite |
| Behavioral | how objects interact and share responsibility | Strategy, Observer, Command, Template Method, Iterator |
Singleton (thread-safe)
Guarantees a single instance with a global access point. The cleanest thread-safe form in Java is an enum, which the JVM initializes lazily and serializes safely.
public enum Config {
INSTANCE;
private final Map<String, String> values = new ConcurrentHashMap<>();
public String get(String key) { return values.get(key); }
}
For a class-based version, use the initialization-on-demand holder idiom — lazy, lock-free, and thread-safe via class-loading semantics:
public final class Cache {
private Cache() {}
private static class Holder { static final Cache INSTANCE = new Cache(); }
public static Cache getInstance() { return Holder.INSTANCE; }
}
In the JDK:
Runtime.getRuntime()andDesktop.getDesktop()are singletons.
Factory Method
Defers instantiation to a method, decoupling callers from concrete classes.
interface Notifier { void send(String msg); }
record EmailNotifier() implements Notifier { public void send(String m) {/*...*/} }
record SmsNotifier() implements Notifier { public void send(String m) {/*...*/} }
class NotifierFactory {
static Notifier of(String channel) {
return switch (channel) {
case "email" -> new EmailNotifier();
case "sms" -> new SmsNotifier();
default -> throw new IllegalArgumentException(channel);
};
}
}
In the JDK:
Calendar.getInstance(),NumberFormat.getInstance(),Logger.getLogger().
Builder
Constructs complex objects step by step, replacing telescoping constructors and giving you readable, validated, immutable results.
public final class HttpRequest {
private final String url;
private final String method;
private HttpRequest(Builder b) { this.url = b.url; this.method = b.method; }
public static class Builder {
private String url;
private String method = "GET";
public Builder url(String url) { this.url = url; return this; }
public Builder method(String m) { this.method = m; return this; }
public HttpRequest build() {
Objects.requireNonNull(url, "url");
return new HttpRequest(this);
}
}
}
HttpRequest req = new HttpRequest.Builder().url("/api").method("POST").build();
In the JDK:
StringBuilder,Stream.Builder, and the realjava.net.http.HttpRequest.Builder.
Strategy
Encapsulates interchangeable algorithms behind a common interface, selected at runtime. In modern Java the strategy is often just a lambda.
@FunctionalInterface
interface DiscountStrategy { double apply(double price); }
class Checkout {
private DiscountStrategy strategy;
Checkout(DiscountStrategy s) { this.strategy = s; }
double total(double price) { return strategy.apply(price); }
}
Checkout black = new Checkout(p -> p * 0.5);
System.out.println(black.total(100));
Output:
50.0
In the JDK:
Comparatorpassed toCollections.sort, and any functional interface argument.
Observer
Notifies a set of dependents when a subject’s state changes — the basis of event-driven and reactive systems.
interface Observer { void update(String event); }
class EventBus {
private final List<Observer> observers = new ArrayList<>();
public void subscribe(Observer o) { observers.add(o); }
public void publish(String event) { observers.forEach(o -> o.update(event)); }
}
EventBus bus = new EventBus();
bus.subscribe(e -> System.out.println("logger: " + e));
bus.publish("user.created");
Output:
logger: user.created
In the JDK:
java.util.concurrent.Flow(reactive streams), Swing/AWT listeners, andPropertyChangeListener. The legacyjava.util.Observerwas deprecated in Java 9.
Best Practices
- Apply a pattern to solve a concrete problem; don’t retrofit one onto code that’s already simple.
- Prefer language features over ceremony: an
enumsingleton, a lambda strategy, or a record beats a hand-rolled class hierarchy. - Program to interfaces (
List,Notifier), not implementations, so patterns can swap concrete types freely. - Keep builders immutable and validate in
build(); never expose a half-constructed object. - Name classes after the pattern only when it aids communication —
RetryStrategyis helpful,UserManagerFactoryImplusually is not.
Interview Questions
Q: Why is an enum the preferred Singleton in Java?
The JVM guarantees a single instance per enum constant, handles lazy initialization and thread safety, and prevents the reflection and serialization attacks that can create extra instances of a conventional singleton.
Q: What’s the difference between Factory Method and Builder? Factory Method chooses and returns which object to create, hiding the concrete type. Builder assembles a single complex object step by step, typically with many optional parameters, returning an immutable result.
Q: How does the Strategy pattern relate to lambdas?
A strategy is a single-method interface whose implementation varies at runtime — exactly a functional interface. Java 8 lambdas let you supply a strategy inline without a named class, which is why Comparator and Runnable are everyday strategies.
Q: Where does the Observer pattern appear in modern Java?
In java.util.concurrent.Flow (the reactive-streams API), GUI event listeners, and frameworks like Spring’s ApplicationEvent. The original Observable/Observer types were deprecated in Java 9 in favor of these.