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.
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
Creational
How objects get made. They hide construction so the caller doesn't depend on concrete classes. Singleton, Factory, Builder, Prototype, Abstract Factory.
Structural
How objects are composed. They assemble things into bigger things while keeping them flexible. Decorator, Adapter, Facade, Proxy, Composite, Bridge, Flyweight.
Behavioural
How objects talk and share work. They assign responsibility and communication. Strategy, Observer, Command, Template Method, State, Iterator, Chain of Responsibility.
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.
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
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.
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.
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;
}
}
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.
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.
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");
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 |
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.
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");
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
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.
// 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 |
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.
// 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.
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();
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 place —
build() checks invariants across fields before the object exists.
4. Optional parameters without a combinatorial explosion of constructors.
StringBuilder · Stream.Builder ·
HttpRequest.newBuilder() · Calendar.Builder — and Lombok's
@Builder generates the whole thing from an annotation.
// 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.
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.
}
}
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.
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.
// 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);
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.
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.
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.
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.
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.
// 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.
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.
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.
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 |
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.
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.Observableis 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.