Design Patterns — The Discriminators & Taxonomy (Deep Dive)
The Design Patterns Overview pages (Classification, Introduction) give you the per-pattern tables — what each pattern is, which family it sits in, its structure. What they cannot give you, because a table is flat, is the thing interviewers actually score: can you tell two patterns apart when their class diagrams look identical, or route the right pattern from a one-paragraph scenario in real time. That is a judgment skill, not a recall skill, and it is built by drilling head-to-head discriminators, not by re-reading definitions. This page is that judgment layer — it assumes you already know each pattern's mechanics and cross-references the dedicated pages (Structural Patterns — Discriminators & Senior Gotchas; Singleton — Thread-Safe Variants) rather than re-deriving them.
1. Pattern vs idiom vs architectural pattern vs principle — four different altitudes
"Design pattern" gets used as a catch-all, but interviewers who ask "is that a pattern?" are really asking whether you know which altitude a given idea operates at. Four altitudes, from the rule that governs a single line of code up to the rule that governs whole services:
| Level | Unit of granularity | Portable across languages? | Example |
|---|---|---|---|
| Principle | a single class or method — a lens for judging any design, not a solution to one recurring problem | Yes — a rule of thumb, not code | SOLID (Single Responsibility, Open/Closed, …), DRY, YAGNI |
| Idiom | one language's type system or runtime — solves a problem that exists only because of how that language works | No — does not translate to a language without the same constraint | RAII (C++ destructors tied to scope, for deterministic cleanup without a GC); Go's functional-options pattern (variadic closures, because Go has no constructor overloading or default arguments) |
| Design pattern (GoF) | a handful of classes/objects collaborating — a named, reusable solution to a recurring OO design problem | Yes, across any language with interfaces/classes — though some patterns shrink or vanish once a language gets first-class functions (Strategy) or built-in iteration (Iterator) | Adapter, Strategy, Observer, Builder, … |
| Architectural pattern | whole subsystems, services, or processes — governs how modules or machines relate, not how classes relate | Yes, at the level of a system diagram | MVC, layered architecture, microservices, event-driven architecture, CQRS |
GoF patterns sit in the middle deliberately: they assume an object-oriented type system (interfaces, subclassing, virtual dispatch) but say nothing about processes, deployment, or network boundaries — that is the architectural pattern's job. A principle like Dependency Inversion is even more general: it is the reason Strategy, Factory Method, and Observer all work (depend on an abstraction, not a concrete class), but it does not by itself tell you which pattern to reach for. The interview tell: if the question is "how do I add behavior to this one object at runtime," you're at design-pattern altitude (Decorator); if it's "how do these five services find and call each other," you're at architectural altitude (service discovery, API gateway); if it's "why is this code hard to change," you're at principle altitude (probably an SRP or OCP violation, and the fix may be a pattern — or may just be moving a method).
2. The "structure-identical, intent-different" problem
Several GoF pairs compile to exactly the same UML diagram — a context class holds a reference to an interface, and one or more concrete classes implement it. If structure were what defined a pattern, these pairs would be the same pattern. They are not, because intent and context define a pattern, not the shape of the class diagram. The senior answer to "what pattern is this?" is never "count the boxes" — it is "what is this code trying to let vary, and who decides when."
| Pair | Shared structure | The intent that separates them |
|---|---|---|
| Bridge vs Strategy | a class holds an interface reference; multiple concrete classes implement it | Strategy swaps ONE algorithm for ONE operation, chosen per call — a single, tactical axis of variation. Bridge spans TWO orthogonal hierarchies designed together up front (an abstraction axis and an implementation axis) so any abstraction can pair with any implementation. One axis, swapped locally → Strategy. Two axes, planned architecturally → Bridge. (Full treatment, including the M×N vs M+N class-count math: Structural Patterns — Discriminators & Senior Gotchas.) |
| State vs Strategy | a context class holds an interface reference; concrete classes implement the behavior | Strategy's variants are interchangeable and client-chosen — the caller picks the comparator or payment processor, and nothing about the object itself changes. State's variants are self-driven — the current state object decides, from inside itself, when to hand the context off to the next state (e.g. a TCP connection moving from Listen to SynReceived to Established), and the context is typically unaware a transition even happened. The test: does anything outside the object choose the variant per call (Strategy), or does the object silently swap its own behavior as a side effect of handling a request (State)? |
| Command vs Strategy | an interface with a single abstract method, multiple concrete implementations, held by reference | Strategy encapsulates an algorithm — it is invoked synchronously, inline, as one step of a larger operation, and carries no notion of "undo" or "when." Command encapsulates a request as a first-class object — receiver, action, and parameters are bundled together so the request can be queued, logged, retried, or undone independently of when it was created. The test: could this object sit in a queue for an hour, get serialized to disk, or be reversed after the fact? If yes, that's Command's job, not Strategy's. |
3. The wrap-by-interface trap: Adapter vs Decorator vs Proxy
This is the single most exploited confusion in structural patterns, because all three wrap one object behind an interface the client already holds — and two of the three (Decorator, Proxy) don't even change that interface. (Full mechanics, code, and the caching-proxy concurrency hazard live on the Structural Patterns — Discriminators & Senior Gotchas page; this is the compressed discriminator.)
- Adapter changes the interface. The client expects interface A; the object you have exposes interface B; the adapter implements A and translates each call into a call on B. Without the adapter, the client literally cannot compile against the object.
- Decorator keeps the interface and ADDS behavior. Client and wrapped object share the exact same interface; the decorator's job is to run extra logic (compress, log, buffer, encrypt) and then always forward the call onward. Stack N decorators and the call still reaches the real object every time — only the payload transforms along the way.
- Proxy keeps the interface and CONTROLS access. Same interface again, but the proxy's job is to decide whether or how the call reaches the real object at all — lazy-init it, serve a cached answer instead, check a permission and refuse, or forward it over the network. A proxy may legitimately never call the real object.
The distinguishing test, in order: (1) Does the interface change at all? Yes → Adapter, stop here. (2) No — does it add new behavior and unconditionally forward? → Decorator. (3) No — does it decide whether/how the call reaches the real thing, sometimes skipping it? → Proxy.
4. Builder vs Factory — the senior's actual decision rule
The overview's per-pattern tables list Builder's pros ("readable step-by-step construction") and Factory's pros ("hides the concrete class") in isolation. Side by side, the decision collapses to one question: is the hard part WHICH type to create, or HOW to assemble ONE complex object?
- Factory (Method / Abstract Factory) answers "which concrete class." The caller wants a
PaymentGatewayand doesn't care whether it gets aStripeGatewayor aPayPalGateway— the decision is a branch over TYPE, made once, and the resulting object is typically simple to construct (a handful of fields, one obvious way to build it). - Builder answers "how do I assemble ONE type that has many optional pieces." The type is already fixed — there's exactly one
HttpRequestclass — but it might have 15 optional fields (headers, timeout, retry policy, body, …), and most call sites only set 2–3 of them. Builder's job is making that assembly readable and safe to get partially wrong (a validation step at.build()), not choosing between rival implementations.
What breaks if you just call new? The motivating failure is the telescoping constructor. Say HttpRequest has 7 optional parameters (headers, timeout, retries, followRedirects, proxy, compression, auth). To cover every combination with plain constructors you either (a) write one constructor with all 7 parameters, forcing every caller to pass placeholder values (null, 0, false, …) for the ones they don't need — unreadable and easy to swap two adjacent int/boolean args by position — or (b) write an overload per popular subset, and the overload count explodes combinatorially long before you've covered every subset anyone actually needs.
// (a) telescoping constructor — readable? safe? no and no.
new HttpRequest(url, null, 30_000, 3, false, null, false, null);
// ^headers ^retries ^proxy ^auth
// easy to swap 30_000 (timeout ms) and 3 (retries) by accident — both are plain ints
// (b) Builder: named steps, only set what you need, validate once at build()
HttpRequest req = HttpRequest.builder()
.url(url)
.timeout(30_000)
.retries(3)
.header("Authorization", token)
.build(); // build() can reject invalid combinations (e.g. retries<0)
The decision rule: reach for Factory when the branch is over TYPE and the object is cheap to construct once you know the type. Reach for Builder when there is ONE type with many optional parameters or a multi-step assembly order that matters (e.g. must set the connection before setting the read timeout) — telescoping constructors or a wall of setters on a mutable object are the concrete tell. The two compose: a Factory can return the correct concrete class, and that class can expose a Builder for constructing it.
5. Singleton's thread-safety mechanic — the single most-asked overview question
What breaks with the naive lazy version: if (instance == null) instance = new Singleton(); is a read-modify-write race — two threads can both read null before either writes, and both construct an instance, silently breaking the "exactly one" guarantee (full trace and diagram: Singleton — Thread-Safe Variants). The two production-grade fixes:
// Double-checked locking — lock only on first init; volatile is MANDATORY,
// not just for atomicity but so no thread ever sees a non-null-but-half-constructed object
// (a memory-model reordering hazard, not merely a race on the null check).
private static volatile Singleton instance;
static Singleton get() {
if (instance == null) { // fast path: no lock once initialized
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton(); // re-check under the lock
}
}
return instance;
}
// Bill Pugh holder idiom — lazy AND thread-safe with NO synchronization at all;
// the classloader itself guarantees the inner class initializes exactly once, on first access.
private static class Holder { static final Singleton INSTANCE = new Singleton(); }
static Singleton get() { return Holder.INSTANCE; }
// Enum — Joshua Bloch's recommended default: JVM-spec-immune to both reflection
// and serialization attacks that can still puncture the holder/DCL variants.
enum Singleton { INSTANCE; }
Default to the holder idiom or enum; reach for DCL only when construction genuinely needs a runtime parameter that isn't known at class-load time. This is the single question every "explain a design pattern" overview interview reaches for, precisely because it is a real concurrency bug hiding behind four lines of textbook code — see the dedicated page for the reflection/serialization hardening (readResolve(), constructor guards) and the full variant comparison table.
Pitfalls
- Memorizing the UML instead of the intent. Bridge/Strategy, State/Strategy, and Command/Strategy all draw the same box-and-arrow diagram. An answer that describes the diagram without naming who decides the variant, and when, has not actually distinguished the patterns — it has redrawn them.
- Answering "Adapter vs Decorator vs Proxy" by re-explaining each pattern in isolation instead of running the three-question test in order (interface change? → always-forward? → may-skip?). Isolated explanations sound plausible but don't prove you can route a new scenario.
- Reaching for Builder because "it looks more senior" when there is really only one branch over type and no optional-parameter explosion — that's a Factory (or just a constructor), and an unnecessary Builder is pure ceremony.
- Reaching for a full Abstract Factory when there is only one concrete family in sight "in case we need another vendor later" — YAGNI; a simple Factory Method (or even a constructor) is enough until a second family is real, mirroring the Bridge N=1 trap on the structural page.
- Treating Singleton's naive lazy-init as merely "not thread-safe" without being able to name the exact race (two threads, both read null, both construct) and why
volatilespecifically fixes visibility of a fully-constructed object, not just atomicity of the null check. - Confusing altitude — calling MVC a "design pattern" or calling RAII portable to Java (Java has no destructors; the idiom's very premise doesn't exist there) signals the taxonomy in §1 was never internalized.
Judgment layer — the calls a senior makes out loud
- Bridge vs Strategy: is there really a second, independently-varying axis designed in up front (Bridge), or one interchangeable behavior on an otherwise fixed object (Strategy)?
- State vs Strategy: does the object switch its own behavior as a side effect of handling a request (State), or does the caller choose the variant every time (Strategy)?
- Command vs Strategy: does this need to be queued, logged, or undone later (Command), or is it invoked inline as one step of a larger operation (Strategy)?
- Adapter vs Decorator vs Proxy: interface changes → Adapter; same interface + always forwards + adds behavior → Decorator; same interface + may not forward at all → Proxy.
- Builder vs Factory: branch over WHICH type → Factory; one type, many optional pieces or ordered assembly steps → Builder.
- Singleton: default to holder idiom or enum; reach for double-checked locking only when initialization needs a runtime parameter; never ship the naive lazy version under concurrency.
- Any pattern vs no pattern: if you can't state the recurring problem in one sentence, you're pattern-hunting — plain code that reads well beats a pattern applied for its own sake.
Takeaways
- Structure never defines a pattern — intent and context do. Bridge/Strategy, State/Strategy, and Command/Strategy each share a class diagram but answer a different "who decides, and when."
- Wrap-by-interface has a strict three-question order: does the interface change (Adapter)? If not, does it always forward while adding behavior (Decorator)? If not, does it gate whether the call happens at all (Proxy)?
- Builder and Factory solve different problems — WHICH type (Factory) vs HOW to assemble ONE complex type (Builder) — and the telescoping constructor is the concrete tell that motivates Builder.
- Singleton's naive lazy-init is a genuine data race; the holder idiom or enum are the defaults, DCL needs
volatilefor visibility (not just atomicity), and reflection/serialization need separate hardening the enum gets for free.
Related pages
- Structural Patterns — The Discriminators & Senior Gotchas (Deep Dive) — full mechanics behind the Adapter/Decorator/Proxy and Bridge discriminators compressed here.
- Singleton — Thread-Safe Variants (DCL, Holder, Enum) — the full race trace, memory-model detail, and reflection/serialization hardening behind §5.
- Classification of Design Patterns — the per-pattern family/structure tables this page assumes as prerequisite.
- OOD Foundations & SOLID — The Precise Forms & Interview Tripwires (Deep Dive) — the SOLID principles, including Dependency Inversion, that explain why these patterns work.
Re-authored/Deepened for this guide.
🤖 Don't fully get this? Learn it with Claude
Stuck on Design Patterns — The Discriminators & Taxonomy (Deep Dive)? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Design Patterns — The Discriminators & Taxonomy (Deep Dive)** (OO & Low-Level Design) and want to truly understand it. Explain Design Patterns — The Discriminators & Taxonomy (Deep Dive) from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Design Patterns — The Discriminators & Taxonomy (Deep Dive)** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Design Patterns — The Discriminators & Taxonomy (Deep Dive)** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Design Patterns — The Discriminators & Taxonomy (Deep Dive)** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.