Worked examples — Design Patterns — Creational - Singleton, Factory Method, Abstract Factory, Builder, Prototype
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 classX.- Boolean flag = a value that is only
trueorfalse— 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 holdingPointobjects. 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.

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.
- Make the constructor
private. Why this step? If anyone can callnew Config(), we cannot guarantee "exactly one". Locking the door is the only way to enforce cardinality = 1. - Add a
staticfield to hold the single object.staticmeans "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. - 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.
- Trace both threads through
instance == null. Thread 1 checks:null→ true. Before it assigns, the OS pauses it. Thread 2 checks: stillnull→ true. Why this step? The bug is a timing bug; you only see it by interleaving the two timelines. - 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. - Fix with the holder idiom. A
staticnested 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.
- Find the variation point.
render()needs aButtonbut the type changes per platform. Thatnew WinButton()is the thing that varies. Why this step? You can only abstract a decision once you've located it. - Replace
newwith an abstract methodcreateButton(). Why this step? An abstract method is a hole the subclass must fill — it pushes the choice downward. - 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.
- 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.
- Each concrete factory returns one theme's products. Why this step? Swapping the factory swaps the entire family at once.
- Client takes a
GUIFactory, never a concrete class. Why this step? At runtime you passWinFactoryorMacFactory; 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.
- Make a
Builderobject that holds partial state. Why this step? Named methods (.addOlives()) replace positional booleans, so order and meaning are explicit. - 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. 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.
- 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.
- 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. - 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.
- 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.
- 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.
- 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.
- Matched theme set → Abstract Factory. Why this step? Enemy and item must be consistent (Cell D).
- 12 optional traits → Builder. Why this step? Avoid a 12-argument constructor (Cell E).
- 1000 near-identical orcs → Prototype. Why this step? Repeated expensive construction; clone a template (Cell J).
- 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.
- 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.
- 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. - 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.
- Cost of
newapproach: 10 per orc. Total . Why this step? Each fresh build repeats the expensive parse. - 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.
- 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.

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.