2.2.10 · D3Design Principles

Worked examples — Design Patterns — Creational - Singleton, Factory Method, Abstract Factory, Builder, Prototype

3,462 words16 min readBack to topic

Before any code, a few words spelled out from zero — nothing below assumes you've seen Java:

  • Instance = one actual object living in memory.
  • Class = the blueprint an object is stamped from.
  • Concrete class = a specific, buildable blueprint (not an interface).
  • Interface = a promise of methods with no bodies (a to-do list a class must fill in).
  • Client = whatever code uses the object but shouldn't know how it was built.
  • new X() = the built-in command that stamps out one fresh object of class X.
  • Boolean flag = a value that is only true or false — often passed as a yes/no switch, e.g. "add cheese? true".
  • Method chaining = writing a.step1().step2().step3() on one line, where each step returns the same object so the next step can hang off it, like carriages on a train.
  • Generic List<Point> = a list whose slots are all of one type; here, a list holding Point objects. The <...> just names what's inside.

Keep these handy — every later symbol is built from them.


The scenario matrix

Design-pattern problems are not endless — they fall into a small grid of case classes. Every row names one kind of situation; if you can place a new problem into a cell here, you already know which example to imitate.

The "Which axis?" column tells you which of the two big questions the cell is really about: How many objects? (the cardinality question) or How hard to assemble one? (the composition question). Some cells (word problems, exam twists, scaling) stress-test both at once.

Cell Which axis? The situation Pattern that fits Worked in
A How many (=1) Need one shared object, many callers Singleton Ex 1
B How many (=1, concurrent) Two threads race to create it Singleton (thread-safe) Ex 2
C How hard (choose the class) Base logic fixed, subclass picks class Factory Method Ex 3
D How hard (a family) Several products must match Abstract Factory Ex 4
E How hard (many parts) Constructor has too many params Builder Ex 5
F How hard (degenerate input) Missing/invalid input at build time Builder validation Ex 6
G How many (a copy) Nested mutable field shared by clone Prototype (deep copy) Ex 7
H Both (word problem) Pick the right pattern(s) yourself Mixed Ex 8
I Both (exam twist) "What's wrong with this code?" Singleton pitfall Ex 9
J Both (limiting behaviour) Cost as N objects grows large Prototype vs new Ex 10

The two axes worth naming out loud:

  • Cardinality axis ("how many?") — exactly 1 → Singleton; one-per-request → Factory; many copies → Prototype.
  • Composition axis ("how hard to assemble one?") — simple → constructor; many optional parts → Builder; a matching set → Abstract Factory.

How to read Figure 1 (below): it draws exactly those two axes. The horizontal axis is assembly complexity — how hard it is to build ONE object, growing left-to-right. The vertical axis is cardinality — how many objects you want, growing bottom-to-top. Each pattern is a coloured dot placed where it lives on this grid: orange Singleton sits high up (cardinality = exactly 1) and far left (a single simple object); red Builder sits far right (very complex assembly) and low (you make each one fresh); green Abstract Factory sits right-of-centre (a whole matching set is complex) and mid-height (a family, more than one product). To place any new problem, ask the two axis questions — "how many? how hard to build one?" — and find the nearest dot.

Figure — Design Patterns — Creational -  Singleton, Factory Method, Abstract Factory, Builder, Prototype
Figure 1 — The scenario matrix. X-axis: assembly complexity (harder to build one → right). Y-axis: cardinality (want more → up). Dots: Singleton (orange), Factory Method (blue), Prototype (gray), Abstract Factory (green), Builder (red).

All ten cells (A–J) are worked below; each is a dot or a stress-test of this grid.


Example 1 — Cell A: exactly one, single-threaded

Forecast: before reading on — how many new calls should the client be able to make? Guess the number, then check.

  1. Make the constructor private. Why this step? If anyone can call new Config(), we cannot guarantee "exactly one". Locking the door is the only way to enforce cardinality = 1.
  2. Add a static field to hold the single object. static means "one slot shared by the whole class, not one per instance." Why this step? We need a place to keep the instance so we can return the same one every time.
  3. Add a static getInstance() that builds on first call, returns thereafter. Why this step? Callers still need a way in; this is the single global access point.
class Config {
    private static Config instance;      // the one slot
    private Config() { /* load file */ } // door locked
    public static Config getInstance() {
        if (instance == null)            // build lazily, once
            instance = new Config();
        return instance;
    }
}

Verify: Config.getInstance() == Config.getInstance() must be true (same object). The number of new Config() calls a client can make directly = 0. Correct — that was the forecast target.


Example 2 — Cell B: exactly one, but two threads race

First, one term from zero. The JVM (Java Virtual Machine) is the program that actually runs your compiled Java code — think of it as the engine underneath. It has strict, published rules about when it loads a class into memory, and we're about to lean on one of those guarantees.

Forecast: how many instances can the naive code create in the worst case? Guess before step 3.

  1. Trace both threads through instance == null. Thread 1 checks: null → true. Before it assigns, the OS pauses it. Thread 2 checks: still null → true. Why this step? The bug is a timing bug; you only see it by interleaving the two timelines.
  2. Both now run new Config(). Two objects are born; two calls will later disagree on which is "the" instance. Why this step? This is the concrete failure — cardinality silently becomes 2.
  3. Fix with the holder idiom. A static nested class is loaded by the JVM (Java Virtual Machine) once, lazily, and thread-safely — the language itself guarantees this loading happens a single time no matter how many threads ask. Why this step? We delegate the hard concurrency guarantee to the JVM instead of writing fragile locks by hand.
class Config {
    private Config() {}
    private static class Holder { static final Config I = new Config(); }
    public static Config getInstance() { return Holder.I; }
}

Verify: worst-case instances for naive code = 2 (matches forecast). With the holder idiom, Holder initializes exactly once → instances = 1, regardless of thread count.


Example 3 — Cell C: choose ONE product via subclass

Forecast: in the base class render(), how many times will the word new appear? Guess.

  1. Find the variation point. render() needs a Button but the type changes per platform. That new WinButton() is the thing that varies. Why this step? You can only abstract a decision once you've located it.
  2. Replace new with an abstract method createButton(). Why this step? An abstract method is a hole the subclass must fill — it pushes the choice downward.
  3. Each subclass overrides createButton(). Why this step? Now the shared workflow lives once in the base; only the one choice differs.
abstract class Dialog {
    abstract Button createButton();          // the hook
    void render() { createButton().draw(); } // no concrete class here
}
class WinDialog  extends Dialog { Button createButton(){ return new WinButton(); } }
class WebDialog  extends Dialog { Button createButton(){ return new HtmlButton(); } }

Verify: count of new in Dialog.render() = 0 (forecast). Number of subclasses needed to support 2 platforms = 2. The base class compiles knowing zero concrete button classes. ✓


Example 4 — Cell D: a matching family

Forecast: if we used two separate Factory Methods instead, how many ways could a client accidentally mismatch a pair? Guess.

  1. Group the creators into one interface. Why this step? One object producing the whole set makes "consistency" structural, not a rule you hope callers remember.
  2. Each concrete factory returns one theme's products. Why this step? Swapping the factory swaps the entire family at once.
  3. Client takes a GUIFactory, never a concrete class. Why this step? At runtime you pass WinFactory or MacFactory; the client code is identical.
interface GUIFactory { Button createButton(); Checkbox createCheckbox(); }
class WinFactory implements GUIFactory {
    public Button   createButton(){ return new WinButton(); }
    public Checkbox createCheckbox(){ return new WinCheckbox(); }
}

Verify: with one Abstract Factory, mismatched pairs possible = 0. With two independent factory methods and 2 themes, mismatched combos = 2×2 − 2 matching = 2 possible bad pairings — exactly the risk Abstract Factory removes. ✓


Example 5 — Cell E: too many optional parameters

Forecast: in new Pizza("L", true, false, true, false), which boolean is "olives"? If you had to pause to count, that's the disease Builder cures.

Recall the vocabulary: a boolean flag is a bare true/false switch, and method chaining hangs each step off the previous one because every step returns the same builder object.

  1. Make a Builder object that holds partial state. Why this step? Named methods (.addOlives()) replace positional booleans, so order and meaning are explicit.
  2. Each step returns this (method chaining). this = "the same builder object again", so the next .step() can attach. Why this step? Chaining reads like a sentence and lets you supply only the steps you want.
  3. build() produces the final immutable object. Immutable = cannot be changed after creation. Why this step? No half-built pizza ever escapes; the object is complete the instant a client sees it.
Pizza p = new Pizza.Builder()
    .size("L").addCheese().addOlives().build();

Verify: number of positional booleans the client must remember = 0. Toppings actually added above = cheese + olives = 2; mushrooms and sauce = not set (default off). Reading the code tells you this with zero counting. ✓


Example 6 — Cell F: degenerate input caught at build

Forecast: at how many places should validation live: at every step, or exactly one? Guess.

  1. Let setters just record intent, no checks. Why this step? Mid-build a pizza is allowed to be incomplete; erroring early would forbid legal ordering of steps.
  2. Put all invariant checks inside build(). An invariant = a rule that must always hold for a finished object (here: "size must be present"). Why this step? build() is the single moment the object becomes "real", so it's the one honest place to reject bad state.
  3. Throw if size == null. null = "no value at all" — the empty/missing case. Why this step? A required field missing is a degenerate input — the empty/zero case the matrix demands we handle.
public Pizza build() {
    if (size == null) throw new IllegalStateException("size required");
    return new Pizza(this);
}

Verify: validation points = 1 (inside build()). A new Pizza.Builder().addCheese().build() (no size) → throws. A builder with size set → succeeds. Degenerate case handled at exactly one gate. ✓


Example 7 — Cell G: the shallow-copy trap

Forecast: with a plain shallow copy, does editing the clone's vertices also edit the original's? Yes or no — guess.

  1. Shallow copy: copy the reference, not the list. A reference = an arrow pointing at where the list lives; both objects now point at the same list in memory. Why this step? This is the default of naive cloning and the source of the bug.
  2. Edit the clone's list. Because it's one shared list, the original sees the change too — corruption. Why this step? Demonstrates the failure the matrix's "shallow trap" cell warns about.
  3. Fix: deep copy — make a fresh list of fresh points. Why this step? Independent objects must own independent mutable parts.
public Polygon clone() {
    Polygon c = new Polygon();
    for (Point p : this.vertices)
        c.vertices.add(new Point(p.x, p.y)); // fresh points → deep copy
    return c;
}

Verify: with shallow copy, clone and original share 1 list → editing clone changes original (true). With deep copy, they own 2 separate lists → editing clone leaves original unchanged (false). ✓


Example 8 — Cell H: real-world word problem

Forecast: how many distinct patterns will this one feature use? Guess a number 1–5.

  1. Matched theme set → Abstract Factory. Why this step? Enemy and item must be consistent (Cell D).
  2. 12 optional traits → Builder. Why this step? Avoid a 12-argument constructor (Cell E).
  3. 1000 near-identical orcs → Prototype. Why this step? Repeated expensive construction; clone a template (Cell J).
  4. One global score → Singleton. Why this step? Single source of truth (Cell A).

Verify: distinct patterns used = 4 (Abstract Factory, Builder, Prototype, Singleton). Factory Method is not needed here because we choose whole families (via Abstract Factory), not a single product per subclass. Forecast target = 4. ✓


Example 9 — Cell I: exam twist "what's wrong?"

Forecast: is "everything a Singleton" good design? Yes or no.

  1. Name what a Singleton really is: global mutable state. Global = reachable from anywhere; mutable = can change over time. Why this step? The trap hides behind the nice word "DRY"; naming the true nature exposes it.
  2. Show the cost: hidden dependencies + hard tests. A class using Config.getInstance() inside a method never declares that it needs config — the dependency is invisible, and tests can't substitute a fake. Why this step? Design quality is judged by testability and visible dependencies, not by object count.
  3. Prescribe the fix: Dependency Injection. Pass collaborators in as parameters; reserve Singleton for truly unique resources. Why this step? Injection makes dependencies explicit and swappable.

Verify: correct grade = not good design in general. Number of Singletons this justifies for "truly unique resource" like an app-wide config = 1; making all services Singletons is wrong. ✓


Example 10 — Cell J: limiting behaviour as N grows

Forecast: roughly how many times cheaper is Prototype at N = 1000? Guess an order of magnitude.

  1. Cost of new approach: 10 per orc. Total . Why this step? Each fresh build repeats the expensive parse.
  2. Cost of Prototype: one full build (10) + N cheap clones (1 each). Total . Why this step? The expensive parse happens once; everything after is a cheap copy.
  3. Take the ratio and its limit. ( reads "as grows without bound".) Why this step? The matrix asks for limiting behaviour — what happens as gets huge.

How to read Figure 2 (below): the horizontal axis is , the number of orcs; the vertical axis is total work units. The red line () is the always-build-fresh cost — it climbs steeply and forever. The green line () is the Prototype cost — it barely rises. The two marked dots at show the gap: red at 10000, green at 1010. The steeper the red line stays above the near-flat green, the bigger the win.

Figure — Design Patterns — Creational -  Singleton, Factory Method, Abstract Factory, Builder, Prototype
Figure 2 — Total work vs N. Red = build-fresh (10·N), green = Prototype (10 + N). Dots mark N=1000: red 10000, green 1010. Ratio → 10 as N grows.

Verify: at : new-cost , prototype-cost , ratio . As the ratio tends to 10 — Prototype approaches the full construction-cost ratio. Forecast (≈10×) confirmed. ✓


Wrap-up: every cell covered

All ten matrix cells now have a home:

Cells Covered by Big lesson
A, B Ex 1–2 Enforce cardinality = 1, safely even under threads
C, D Ex 3–4 Vary one product (subclass) vs a whole matching family (composition)
E, F Ex 5–6 Assemble many parts step-by-step; validate once at build()
G Ex 7 Deep-copy mutable fields or the clone corrupts the original
H, I, J Ex 8–10 Real problems, exam traps, and scaling all reduce to the two axes
Recall Quick self-test across the whole matrix

Which cell is "two threads racing to create one object"? ::: Cell B → thread-safe Singleton (holder idiom). A required field is missing when build() runs — where do you catch it? ::: In build() (Cell F), the single validation gate. Clone shares a nested list with its original — what fixes it? ::: A deep copy of the mutable field (Cell G). Abstract Factory vs Factory Method in one word each? ::: Family vs. single product. At N=1000, Prototype (10+N) vs new (10N) — limiting ratio? ::: 10.


See also: Factory Method, Abstract Factory, Builder, Prototype, Singleton, Dependency Injection, Structural Patterns, Behavioral Patterns, and the Design Principles that motivate them all.