Design Patterns in Java — Complete Notes

Singleton, Factory, Builder, Strategy, Observer and Decorator — the six that actually come up. Don't memorise all 23; understand these deeply enough to code them from scratch and to know when not to.

00. What a pattern is (and isn't)

A design pattern is a named, proven solution to a problem that keeps recurring. It is not a library, not code you copy, and definitely not a goal. It's vocabulary.

The real value is the name

Saying "let's use a Builder here" transmits an entire design — the intent, the structure, the trade-offs — in three words. That's the actual payoff: patterns are compressed communication between engineers. The code is secondary.

Patterns are like chess openings. Nobody invented the Sicilian Defence and forced everyone to play it — people noticed the same strong position arising repeatedly and gave it a name. Knowing the name lets two players discuss a whole board state in one word. But you don't play the Sicilian because it's famous; you play it because the position calls for it.

The three categories

CATEGORY 1

Creational

How objects get made. They hide construction so the caller doesn't depend on concrete classes. Singleton, Factory, Builder, Prototype, Abstract Factory.

CATEGORY 2

Structural

How objects are composed. They assemble things into bigger things while keeping them flexible. Decorator, Adapter, Facade, Proxy, Composite, Bridge, Flyweight.

CATEGORY 3

Behavioural

How objects talk and share work. They assign responsibility and communication. Strategy, Observer, Command, Template Method, State, Iterator, Chain of Responsibility.

The two principles behind nearly all of them

1. "Program to an interface, not an implementation." 2. "Favour composition over inheritance." If you deeply understand these two, most patterns become obvious rather than memorised — they're just these ideas applied to a specific problem.

Patternitis is real

The failure mode of learning patterns is applying them everywhere. A SimpleBeanFactoryAwareAspectInstanceFactory is what happens when patterns become the goal. Write the simple thing first. Reach for a pattern when the simple thing has actually started to hurt.

01. Singleton

Intent: guarantee a class has exactly one instance, with one global access point. The most famous pattern, the most abused, and the one most likely to be a trick question.

The naive version, and why it's broken

Lazy singleton — completely broken under concurrency
public class Broken {
    private static Broken instance;
    private Broken() { }                       // private ctor: nobody else can `new` it

    public static Broken getInstance() {
        if (instance == null) {                // check-then-act — a RACE CONDITION
            instance = new Broken();           // two threads can BOTH pass the check
        }
        return instance;
    }
}
// Thread A checks -> null. Thread B checks -> null. Both construct. Two singletons.
Fix 1: synchronize — correct, but slow forever
public static synchronized Broken getInstance() {
    if (instance == null) instance = new Broken();
    return instance;
}
// Correct. But EVERY call locks — including the millions of calls after
// initialisation, where there's nothing left to synchronise. Pure waste.
Fix 2: double-checked locking — correct ONLY with volatile
public class DCL {
    private static volatile DCL instance;      // the `volatile` is NOT optional
    private DCL() { }

    public static DCL getInstance() {
        if (instance == null) {                // 1st check — no lock, the fast path
            synchronized (DCL.class) {
                if (instance == null) {        // 2nd check — someone may have won the race
                    instance = new DCL();
                }
            }
        }
        return instance;
    }
}
Why DCL without volatile is subtly, famously broken

instance = new DCL() is three steps: 1. allocate memory, 2. run the constructor, 3. assign the reference. The JIT is allowed to reorder 2 and 3. Another thread can then see a non-null reference to a half-constructed object, skip the first check, and use it. volatile forbids that reordering. This bug is rare, timing-dependent, and essentially undebuggable — which is why the next two approaches exist.

Fix 3: the initialization-on-demand holder idiom — lazy, fast, no locks
public class Holder {
    private Holder() { }

    private static class Lazy {                 // a nested class isn't loaded until first USED
        static final Holder INSTANCE = new Holder();
    }

    public static Holder getInstance() {
        return Lazy.INSTANCE;                   // triggers Lazy's initialisation, right here
    }
}
// The JVM ALREADY guarantees class initialisation is thread-safe and happens exactly once.
// So we get laziness and thread safety for free, with zero synchronisation on any call.
// No volatile, no locks, no cleverness. Just leaning on the classloader's own guarantees.
Fix 4: the enum singleton — the best one (Effective Java, Item 3)
public enum Config {
    INSTANCE;

    private final Map<String,String> values = new HashMap<>();

    public String get(String key) { return values.get(key); }
}

// Use:
Config.INSTANCE.get("db.url");
Why enum wins

It's the only version immune to the two attacks that break all the others. Reflection: setAccessible(true) on a private constructor lets anyone construct a second instance — but the JVM forbids reflective construction of enums outright. Serialization: deserialising a normal singleton creates a new object every time — but enums are serialised by name and always resolve back to the same constant. Plus it's thread-safe and lazy with no code at all.

Approach Thread-safe Lazy Reflection-proof Verdict
Naive lazy No Yes No Broken
Eager static final Yes No No Fine if construction is cheap
synchronized method Yes Yes No Correct but needlessly slow
Double-checked + volatile Yes Yes No Works; easy to get wrong
Static holder Yes Yes No Best classic idiom
Enum Yes Yes Yes Best overall
Singleton is widely considered an anti-pattern — know why

It's global mutable state in a dinner jacket. It makes unit testing painful (you can't inject a fake, and state leaks between tests), hides dependencies (a method's signature says nothing about the singletons it reaches for), and couples every caller to a concrete class. The modern answer is dependency injection: let Spring/Guice manage a single instance and hand it to you. You get one instance where it matters, without the global. Note a Spring @Singleton bean is one-per-container, not the GoF pattern.

02. Factory

Intent: create objects without the caller naming the concrete class. new hardcodes a dependency; a factory turns "which class?" into a decision made in one place.

Simple Factory — not technically a GoF pattern, but the most used
interface Notification { void send(String msg); }

class EmailNotification implements Notification {
    public void send(String msg) { System.out.println("Email: " + msg); }
}
class SmsNotification implements Notification {
    public void send(String msg) { System.out.println("SMS: " + msg); }
}
class PushNotification implements Notification {
    public void send(String msg) { System.out.println("Push: " + msg); }
}

class NotificationFactory {
    static Notification create(String type) {
        return switch (type) {
            case "email" -> new EmailNotification();
            case "sms"   -> new SmsNotification();
            case "push"  -> new PushNotification();
            default -> throw new IllegalArgumentException("unknown type: " + type);
        };
    }
}

// Caller doesn't know or care which class it gets:
Notification n = NotificationFactory.create(config.getType());
n.send("hello");
What you actually bought

The new is now in one place. Adding a SlackNotification means touching one file — every caller keeps working, untouched. Without it, new EmailNotification() is scattered across forty files and every one is welded to a concrete class.

Factory Method — let subclasses decide

The real GoF pattern: the base class defines the flow, subclasses pick the type
abstract class Dialog {

    protected abstract Button createButton();   // THE factory method — subclasses answer this

    public void render() {                      // the algorithm lives here, and doesn't change
        Button b = createButton();
        b.onClick(this::close);
        b.render();
    }
    void close() { }
}

class WindowsDialog extends Dialog {
    protected Button createButton() { return new WindowsButton(); }
}
class WebDialog extends Dialog {
    protected Button createButton() { return new HtmlButton(); }
}

// render() is written ONCE and works for every platform.
// The subclass supplies only the one varying decision.
Abstract Factory — a family of related products
// When products must MATCH each other — you must never mix Windows and Mac widgets
interface GuiFactory {
    Button   createButton();
    Checkbox createCheckbox();
}

class WindowsFactory implements GuiFactory {
    public Button   createButton()   { return new WindowsButton(); }
    public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
class MacFactory implements GuiFactory {
    public Button   createButton()   { return new MacButton(); }
    public Checkbox createCheckbox() { return new MacCheckbox(); }
}

// Choose the family ONCE; every product is guaranteed consistent thereafter.
GuiFactory f = isWindows ? new WindowsFactory() : new MacFactory();
Button b = f.createButton();
Checkbox c = f.createCheckbox();     // cannot accidentally be a MacCheckbox
Answers Mechanism
Simple Factory "Give me one of these, based on this input" A method with a switch
Factory Method "Subclass, which concrete type do you want?" Inheritance — one product
Abstract Factory "Give me a whole matching family" Composition — many products
Static factory methods — the everyday cousin

List.of(), Optional.of(), Integer.valueOf(), LocalDate.now(). Not the GoF pattern, but the most common "don't call new" technique — and often the better default. Unlike constructors they have names, can return a cached instance (Integer.valueOf caches −128..127), and can return a subtype. See Effective Java, Item 1.

03. Builder

Intent: construct a complex object step by step. It exists to kill the telescoping constructor — the thing that happens when a class has more than about four fields.

The problem — can you spot the bug?
// Telescoping constructors: the classic disease
public Pizza(int size) { ... }
public Pizza(int size, boolean cheese) { ... }
public Pizza(int size, boolean cheese, boolean pepperoni) { ... }
public Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) { ... }

new Pizza(12, true, false, true);
//            ^^^^  ^^^^^  ^^^^  which one is bacon again?

// And this compiles perfectly, silently doing the wrong thing:
new Pizza(12, false, true, true);   // meant cheese, got pepperoni. No warning. Ever.
The Builder — named, safe, immutable, order-independent
public class Pizza {
    private final int size;                 // all final -> immutable & thread-safe
    private final boolean cheese;
    private final boolean pepperoni;
    private final boolean bacon;

    private Pizza(Builder b) {              // private ctor: the Builder is the ONLY way in
        this.size      = b.size;
        this.cheese    = b.cheese;
        this.pepperoni = b.pepperoni;
        this.bacon     = b.bacon;
    }

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

    public static class Builder {
        private final int size;             // required -> in the Builder's constructor
        private boolean cheese;             // optional -> sensible defaults
        private boolean pepperoni;
        private boolean bacon;

        public Builder(int size) { this.size = size; }

        public Builder cheese()    { this.cheese = true; return this; }     // return this -> chain
        public Builder pepperoni() { this.pepperoni = true; return this; }
        public Builder bacon()     { this.bacon = true; return this; }

        public Pizza build() {
            if (size < 6) throw new IllegalArgumentException("size must be >= 6");  // validate HERE
            return new Pizza(this);         // the object is never seen in a partial state
        }
    }
}

// Now read this and tell me it's ambiguous:
Pizza p = Pizza.builder(12)
    .cheese()
    .bacon()
    .build();
The four things Builder buys you

1. Readability — every argument is named at the call site. 2. Immutability — all fields final, set once, so the result is automatically thread-safe. 3. Validation in one placebuild() checks invariants across fields before the object exists. 4. Optional parameters without a combinatorial explosion of constructors.

Builders in the wild

StringBuilder · Stream.Builder · HttpRequest.newBuilder() · Calendar.Builder — and Lombok's @Builder generates the whole thing from an annotation.

With records — you often don't need one at all
// For 2-3 fields a record is simply better. Don't reach for a Builder reflexively.
record Point(int x, int y) { }

// The Builder earns its keep once there are many optional fields —
// and records pair with it nicely for the validation:
record Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) {
    Pizza {
        if (size < 6) throw new IllegalArgumentException("size must be >= 6");
    }
    static Builder builder(int size) { return new Builder(size); }
    // ... Builder as above, calling the canonical constructor in build()
}

04. Strategy

Intent: define a family of interchangeable algorithms, encapsulate each one, and make them swappable at runtime. In modern Java this is usually one lambda.

The problem — a switch that grows forever
class Checkout {
    double pay(double amount, String method) {
        if (method.equals("card")) {
            // 20 lines of card logic
        } else if (method.equals("paypal")) {
            // 20 lines of PayPal logic
        } else if (method.equals("crypto")) {
            // 20 lines of crypto logic
        }
        // Adding "applepay" means EDITING this class. Again.
        // It violates Open/Closed: you can't extend it without modifying it.
        // And it's untestable in isolation, and 300 lines long.
    }
}
Strategy — each algorithm is its own object
interface PaymentStrategy {              // the strategy interface
    void pay(double amount);
}

class CardPayment implements PaymentStrategy {
    private final String cardNumber;
    CardPayment(String cardNumber) { this.cardNumber = cardNumber; }
    public void pay(double amount) { System.out.println("Card: " + amount); }
}
class PayPalPayment implements PaymentStrategy {
    public void pay(double amount) { System.out.println("PayPal: " + amount); }
}

class Checkout {                          // the CONTEXT — knows the interface, not the classes
    private PaymentStrategy strategy;
    void setStrategy(PaymentStrategy s) { this.strategy = s; }
    void checkout(double amount) { strategy.pay(amount); }   // just delegate
}

Checkout c = new Checkout();
c.setStrategy(new CardPayment("4111..."));
c.checkout(99.99);
c.setStrategy(new PayPalPayment());       // swapped at RUNTIME
c.checkout(50.00);

// Adding ApplePay = adding ONE new class. Checkout is never touched. That's Open/Closed.
Strategy is composition beating inheritance

The inheritance answer would be CardCheckout extends Checkout — which fixes the behaviour at compile time and can never change. Strategy makes behaviour a field, so it can change per instance, per call, per config, at runtime. This is literally the "favour composition over inheritance" principle in pattern form.

In modern Java, a Strategy is usually just a lambda
// PaymentStrategy has ONE abstract method -> it's a functional interface -> lambda territory
Checkout c = new Checkout();
c.setStrategy(amount -> System.out.println("Card: " + amount));
c.setStrategy(amount -> System.out.println("PayPal: " + amount));

// Often you don't even need your own interface:
Comparator<Person> byName = Comparator.comparing(Person::getName);   // a sorting strategy
list.sort(byName);                                                   // ...injected here

// A registry of strategies, no switch anywhere:
Map<String, PaymentStrategy> strategies = Map.of(
    "card",   amount -> chargeCard(amount),
    "paypal", amount -> chargePayPal(amount)
);
strategies.get(method).pay(amount);
You've been using Strategy all along

Comparator passed to sort · Predicate passed to filter · a ThreadFactory given to an executor · a RejectedExecutionHandler. Every "pass me the behaviour" API in the JDK is Strategy.

05. Observer

Intent: when one object changes state, everything that cares is notified automatically. A one-to-many dependency, with the subject knowing nothing about who's listening.

The pattern
interface Observer {                     // what listeners implement
    void update(String event);
}

class Subject {                          // the thing being watched
    private final List<Observer> observers = new CopyOnWriteArrayList<>();  // safe iteration

    public void subscribe(Observer o)   { observers.add(o); }
    public void unsubscribe(Observer o) { observers.remove(o); }   // ESSENTIAL — see gotchas

    protected void notifyAll(String event) {
        for (Observer o : observers) o.update(event);   // fire away
    }
}

class OrderService extends Subject {
    public void placeOrder(Order order) {
        save(order);
        notifyAll("ORDER_PLACED:" + order.getId());     // I don't know or care who listens
    }
}

// Listeners register themselves
OrderService svc = new OrderService();
svc.subscribe(e -> emailService.sendConfirmation(e));
svc.subscribe(e -> analytics.track(e));
svc.subscribe(e -> inventory.reserve(e));
// Adding an SMS notification = one more subscribe(). OrderService is never modified.
The point is the direction of the dependency

Without Observer, OrderService would call emailService, analytics and inventory directly — three hard dependencies, and a fourth every time the business adds a feature. With it, the arrows point inward: listeners depend on the subject, the subject depends on nobody. That's loose coupling, and it's why event-driven architectures scale organisationally.

A newsletter. The publisher doesn't phone each reader; it maintains a subscriber list and sends one broadcast. Readers subscribe and unsubscribe freely. The publisher has no idea who's on the list, and doesn't need to — adding a reader requires zero changes to the publishing process.

Don't use java.util.Observable

It was deprecated in Java 9. It's a class (so it burns your one inheritance slot), it isn't serialisable, it isn't thread-safe, and its notification order is unspecified. Roll your own listener interface (five lines, as above), or use PropertyChangeSupport, an event bus, Spring's ApplicationEventPublisher, or reactive streams.

Observer everywhere you look
button.addActionListener(e -> save());        // Swing/AWT
element.addEventListener("click", handler);   // the DOM
publisher.subscribe(subscriber);              // Flow API (Java 9) / Reactive Streams
@EventListener void onOrder(OrderEvent e) { } // Spring
// Kafka, RabbitMQ, webhooks — pub/sub is Observer at network scale.

06. Decorator

Intent: add behaviour to an object by wrapping it, at runtime, without touching its class. The alternative to an explosion of subclasses.

The problem — the combinatorial subclass explosion
// Coffee, and you want milk, sugar, whipped cream...
class Coffee { }
class CoffeeWithMilk extends Coffee { }
class CoffeeWithSugar extends Coffee { }
class CoffeeWithMilkAndSugar extends Coffee { }
class CoffeeWithMilkAndWhip extends Coffee { }
class CoffeeWithMilkAndSugarAndWhip extends Coffee { }
// ... 3 add-ons -> 8 classes. 10 add-ons -> 1024 classes. It's 2^n. This does not work.
Decorator — wrap instead of subclass
interface Coffee {
    double cost();
    String description();
}

class SimpleCoffee implements Coffee {                    // the CONCRETE COMPONENT
    public double cost() { return 2.00; }
    public String description() { return "coffee"; }
}

abstract class CoffeeDecorator implements Coffee {        // the BASE DECORATOR
    protected final Coffee inner;                         // IS-A Coffee, HAS-A Coffee — the trick
    protected CoffeeDecorator(Coffee inner) { this.inner = inner; }
}

class Milk extends CoffeeDecorator {
    Milk(Coffee inner) { super(inner); }
    public double cost() { return inner.cost() + 0.50; }              // delegate, then ADD
    public String description() { return inner.description() + " + milk"; }
}
class Sugar extends CoffeeDecorator {
    Sugar(Coffee inner) { super(inner); }
    public double cost() { return inner.cost() + 0.20; }
    public String description() { return inner.description() + " + sugar"; }
}

// Compose freely, at runtime, in any order and any depth:
Coffee c = new Sugar(new Milk(new SimpleCoffee()));
System.out.println(c.description());   // coffee + milk + sugar
System.out.println(c.cost());          // 2.70

// 10 add-ons = 10 classes, not 1024. And each one is independently testable.
The structural trick that makes it work

A decorator implements the same interface as the thing it wraps and holds a reference to one. That's why a decorated object is still a Coffee — so callers can't tell, and decorators can wrap decorators infinitely. IS-A and HAS-A at the same time.

Clothing. You don't need a "PersonWearingShirtAndJacketAndScarf" class. You put a shirt on the person, a jacket over the shirt, a scarf over the jacket. Each layer adds warmth and knows only about "the layer underneath" — and from the outside, it's still a person.

The canonical example: java.io is Decorator all the way down
InputStream in =
    new BufferedInputStream(              // adds buffering
        new GZIPInputStream(              // adds decompression
            new FileInputStream("data.gz")));   // the actual source

// Each wrapper adds ONE capability. Reorder or omit at will —
// this is why java.io looks so verbose, and also why it's so flexible.

// Same idea:
new BufferedReader(new InputStreamReader(System.in));
Collections.unmodifiableList(list);        // a decorator that removes a capability
Collections.synchronizedMap(map);          // a decorator that adds locking
Pattern Same interface? Purpose
Decorator Yes Add behaviour
Adapter No — it changes it Convert an incompatible interface
Proxy Yes Control access (lazy load, cache, security, remote)
Facade No — a new, simpler one Simplify a complex subsystem
Decorator vs Proxy — they look identical in code

Both wrap an object behind the same interface. The difference is intent: a decorator adds something the caller wants (buffering, compression); a proxy controls access to something (lazily creating it, caching it, checking permissions, calling it over the network). Same structure, different reason — which is exactly why patterns are about intent, not shape.

07. Choosing — and how they relate

The problem you have The pattern
"There must be exactly one of these" Singleton (but really: dependency injection)
"The caller shouldn't know which class it gets" Factory
"This constructor has eight arguments" Builder
"This if/else chain keeps growing" Strategy
"Several things must react when this happens" Observer
"I need optional features in any combination" Decorator
"This library's interface is wrong for me" Adapter
"I want to intercept calls to this" Proxy
"The steps are fixed, one step varies" Template Method
✓ Do
  • Let a pattern emerge from real pain (usually the third time you copy something).
  • Use the name in conversation and in class names — PaymentStrategy, PizzaBuilder.
  • Prefer the modern Java form: a lambda is a Strategy, a record beats a small Builder.
  • Understand the intent. Decorator and Proxy have identical code.
✗ Don't
  • Apply a pattern because you just learned it.
  • Use Singleton for anything with mutable state — inject it instead.
  • Build a Factory for a class with one implementation.
  • Memorise all 23 — six well-understood beats twenty-three recited.
  • Add indirection "for flexibility" you have no evidence you'll need.
How modern Java changed the picture

Several GoF patterns were workarounds for missing language features. Lambdas made Strategy and Command nearly disappear into a one-liner. Records made small Builders unnecessary. sealed + pattern matching replaces a lot of Visitor. Default methods cover much of Template Method. A pattern-shaped hole in 1994 Java may be a language feature today — which is exactly why you learn the problem, not the boilerplate.

08. Gotchas

1. Double-checked locking without volatile is broken.

The constructor and the reference assignment can be reordered, so another thread can see a non-null reference to a half-built object. The bug is rare, non-deterministic and essentially undebuggable. Use the static holder idiom or an enum and sidestep it entirely.

2. Reflection and serialization both break a normal Singleton.

setAccessible(true) lets anyone call your private constructor; deserialization creates a brand-new object every time (unless you implement readResolve()). Only the enum singleton is immune to both, for free.

3. Observer leaks memory if you never unsubscribe.

The subject holds a strong reference to every listener, so a listener that forgets to unsubscribe can never be garbage collected — the classic "lapsed listener" leak. Always provide and call unsubscribe, or hold listeners weakly.

4. Observer's notification order is not defined — don't rely on it.

If listener B depends on listener A having run first, you don't have an Observer problem, you have a design problem. Also: one listener throwing can abort the notification loop, so later listeners never fire. Catch per-listener.

5. Calling notifyObservers while holding a lock invites deadlock.

You're calling unknown code (an "alien method") with a lock held — you have no idea what locks it will try to take. Copy the listener list under the lock, then notify outside it.

6. A Builder that forgets a required field fails at runtime, not compile time.

That's Builder's one real weakness versus a constructor. Mitigate it by putting required fields in the Builder's constructor (as in the pizza example) and validating the rest in build() — never after the object exists.

7. Decorators make stack traces and debugging genuinely worse.

Five layers of wrapping means five frames of delegation before the real work, and toString()/equals() often don't survive the wrapping. It's the price of the flexibility — worth it for java.io, overkill for two options that never change.

8. Singleton makes tests share state.

A static instance persists across test methods, so tests pass alone and fail in a suite — or pass in one order and fail in another. There's no clean fix beyond a reset hook you shouldn't need. This alone is why DI won.

09. Interview Q&A

Q: What's the best way to implement a Singleton, and why?

An enum — thread-safe, lazy, and the only form immune to reflection and serialization attacks, with no code. If it must be a class, use the static holder idiom, which gets laziness and thread safety from the JVM's own class-initialisation guarantee, with no locking. And mention that in a real app you'd usually prefer DI.

Q: Why does double-checked locking need volatile?

Because new is allocate → construct → assign, and the JIT may reorder the assignment before construction completes. A second thread could then see a non-null, half-built object. volatile forbids the reordering (and gives visibility).

Q: Factory Method vs Abstract Factory?

Factory Method uses inheritance — a subclass overrides one method to decide one product's type. Abstract Factory uses composition — an object whose methods produce a family of related products guaranteed to match. One method vs one object with several methods.

Q: When would you use a Builder?

Many constructor parameters (say 4+), especially optional ones, or when parameters of the same type are easy to swap by accident. It gives named arguments, an immutable result, and a single place to validate cross-field invariants. For 2–3 fields, use a record instead.

Q: Strategy vs inheritance?

Inheritance fixes behaviour at compile time and burns your one superclass. Strategy makes behaviour a field, so it's swappable at runtime, independently testable, and composable. It's "favour composition over inheritance" in pattern form — and in modern Java the strategy is usually just a lambda.

Q: Decorator vs inheritance?

Inheritance needs a class per combination — 2n classes for n features. Decorator needs one class per feature and composes them at runtime, in any order. That's exactly why java.io is built from wrappers.

Q: Decorator vs Proxy — the code looks the same. What's the difference?

Intent. Decorator adds behaviour the caller wants; Proxy controls access to the subject (lazy init, caching, security, remoting). A decorator's target always exists; a proxy often creates or hides it. Structurally identical, semantically different.

Q: Where is Observer used in the JDK?

Swing listeners, PropertyChangeSupport, the Java 9 Flow API (Reactive Streams), Spring's application events. Note java.util.Observable is deprecated since Java 9 — it's a class, not thread-safe, and not serialisable.

Q: Are design patterns still relevant?

The problems are; some of the boilerplate isn't. Lambdas collapsed Strategy and Command into one line, records replace small Builders, sealed types + pattern matching cover much of Visitor. The vocabulary remains extremely valuable — but a pattern-shaped hole from 1994 is often a language feature in 2026.

Q: Which pattern do you dislike, and why?

A great answer is Singleton: it's global mutable state, it hides dependencies (the signature doesn't reveal them), it makes tests share state and fail by ordering, and it couples callers to a concrete class. DI gives you one instance without any of that. Showing you know when not to use a pattern is the point of the question.

10. Cheat sheet

  • Categories: Creational (how objects are made) · Structural (how they're composed) · Behavioural (how they interact).
  • Two principles behind them all: program to an interface · favour composition over inheritance.
  • Singleton: one instance. Best = enum (reflection- & serialization-proof); classic = static holder. DCL needs volatile. Really: use DI.
  • Factory: hide the concrete class. Simple (switch) · Factory Method (inheritance, one product) · Abstract Factory (composition, a family).
  • Builder: kills telescoping constructors. Named args · immutable · validate in build() · required fields in the Builder's ctor. Records beat it when small.
  • Strategy: interchangeable algorithms as objects → kills the growing if/else, gives Open/Closed. Today: a lambda (Comparator, Predicate).
  • Observer: one-to-many auto-notify; subject knows nothing about listeners. Unsubscribe or leak. java.util.Observable is deprecated.
  • Decorator: same interface + holds one → wrap to add behaviour at runtime. n features = n classes, not 2n. See java.io.
  • Confusables: Decorator adds · Proxy controls access · Adapter converts · Facade simplifies.
  • Rule: write the simple thing first. The pattern earns its place the third time the simple thing hurts.
Last reviewed · July 2026 · part of knowledge-base