Knowledge Guide
HomeOO & Low-Level DesignDesign Patterns Overview

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:

LevelUnit of granularityPortable across languages?Example
Principlea single class or method — a lens for judging any design, not a solution to one recurring problemYes — a rule of thumb, not codeSOLID (Single Responsibility, Open/Closed, …), DRY, YAGNI
Idiomone language's type system or runtime — solves a problem that exists only because of how that language worksNo — does not translate to a language without the same constraintRAII (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 problemYes, 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 patternwhole subsystems, services, or processes — governs how modules or machines relate, not how classes relateYes, at the level of a system diagramMVC, 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."

PairShared structureThe intent that separates them
Bridge vs Strategya class holds an interface reference; multiple concrete classes implement itStrategy 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 Strategya context class holds an interface reference; concrete classes implement the behaviorStrategy'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 Strategyan interface with a single abstract method, multiple concrete implementations, held by referenceStrategy 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.)

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.

Three lanes showing Client to interface to Wrapper to Wrapped object for Adapter (interface changes), Decorator (same interface, adds behavior, always forwards), and Proxy (same interface, controls access, may skip the call)
Three lanes showing Client to interface to Wrapper to Wrapped object for Adapter (interface changes), Decorator (same interface, adds behavior, always forwards), and Proxy (same interface, controls access, may skip the call)

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?

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

Judgment layer — the calls a senior makes out loud

Takeaways

Related pages


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.

🎨 Explain it visually

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.
🤔 Walk me through it (interactive)

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.
🧪 Quiz me & fix my gaps

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.
🧠 Make it stick

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.

📝 My notes