All chapters in this track
- 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
- 02Chapter 2: Patterns and SOLID: How They Relate
- 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
- 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
- 05Chapter 5: Structural Patterns I: Adapter & Facade
- 06Chapter 6: Structural Patterns II: Decorator & Proxy
- 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
- 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
- 09Chapter 9: Behavioral Patterns II: Observer & Mediator
- 10Chapter 10: Behavioral Patterns III: Command & Memento
- 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
- 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
- 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
- 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
- 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
- 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
- 17Chapter 17: Architectural Patterns III: The Repository Pattern
- 18Chapter 18: Recognizing Which Pattern Fits an Interview Design Prompt
Chapter 4 of 18
Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
Builder
The problem it solves: constructing an object requires many optional parameters, or the construction happens in meaningful steps — a constructor with ten parameters (many optional, easy to pass in the wrong order) is error-prone and unreadable at the call site.
Before — the "telescoping constructor" problem:
class HttpRequest {
constructor(
public url: string,
public method: string = "GET",
public headers: Record<string, string> = {},
public body: string | null = null,
public timeoutMs: number = 5000,
public retries: number = 0
) {}
}
// Which positional argument is which, at the call site? Error-prone to read and write.
const req = new HttpRequest("https://api.example.com", "POST", {}, '{"a":1}', 10000, 3);
After — Builder makes construction readable and step-by-step:
class HttpRequestBuilder {
private method = "GET";
private headers: Record<string, string> = {};
private body: string | null = null;
private timeoutMs = 5000;
private retries = 0;
constructor(private url: string) {}
setMethod(method: string): this { this.method = method; return this; }
setHeader(key: string, value: string): this { this.headers[key] = value; return this; }
setBody(body: string): this { this.body = body; return this; }
setTimeout(ms: number): this { this.timeoutMs = ms; return this; }
setRetries(count: number): this { this.retries = count; return this; }
build(): HttpRequest {
return new HttpRequest(this.url, this.method, this.headers, this.body, this.timeoutMs, this.retries);
}
}
const req = new HttpRequestBuilder("https://api.example.com")
.setMethod("POST")
.setHeader("Content-Type", "application/json")
.setBody('{"a":1}')
.setTimeout(10000)
.setRetries(3)
.build();
// Self-documenting at the call site — no need to remember parameter order.
class HttpRequestBuilder:
def __init__(self, url: str) -> None:
self._url = url
self._method = "GET"
self._headers: dict[str, str] = {}
self._body: str | None = None
def set_method(self, method: str) -> "HttpRequestBuilder":
self._method = method
return self
def set_header(self, key: str, value: str) -> "HttpRequestBuilder":
self._headers[key] = value
return self
def build(self) -> "HttpRequest":
return HttpRequest(self._url, self._method, self._headers, self._body)
[IN PRACTICE] Python idiomatically often reaches for keyword arguments with defaults instead of a Builder — HttpRequest(url, method="POST", headers={...}) solves the same readability problem with far less ceremony, since Python doesn't have the telescoping-constructor pain TypeScript/Java do (no positional-only ambiguity when keyword arguments are used). Builder is most justified when construction also involves genuine sequencing or validation logic across steps, not just "many optional named parameters" — for that narrower case, a language's native named-parameter support is usually the simpler fix.
When to use it: many optional parameters, and/or construction genuinely happens in ordered steps with validation between them (e.g., a SQL query builder that must add a FROM before a WHERE).
When to avoid it: a handful of parameters, or a language with good native named/keyword argument support that already solves the readability problem without extra classes.
Complexity cost: one additional class per builder, plus per-field setter methods — worthwhile specifically when it's replacing a genuinely error-prone constructor, not as a default habit for every class with more than two fields.
Singleton
The problem it solves: exactly one instance of a class should exist across the entire application (a configuration object, a connection pool, a logger), and every part of the code that needs it should get the same instance.
class AppConfig {
private static instance: AppConfig | null = null;
private constructor(public readonly apiUrl: string) {} // private: can't be `new`'d from outside
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig(process.env.API_URL ?? "http://localhost");
}
return AppConfig.instance;
}
}
const config = AppConfig.getInstance(); // always the same instance, anywhere it's called
class AppConfig:
_instance: "AppConfig | None" = None
def __init__(self, api_url: str) -> None:
self.api_url = api_url
@classmethod
def get_instance(cls) -> "AppConfig":
if cls._instance is None:
cls._instance = cls(api_url="http://localhost")
return cls._instance
[SENIOR INSIGHT] Singleton is, by a wide margin, the most criticized pattern in the entire catalog, and for good reason — it's frequently used as a polite name for global mutable state, which brings all of global state's classic problems: hidden dependencies (a class calling AppConfig.getInstance() internally doesn't declare that dependency anywhere a caller can see it), hard-to-test code (you can't substitute a test double without reaching into the Singleton's internals), and — directly connecting to the OOP book's Chapter 14 — genuine danger under concurrency, since a naive getInstance() has a check-then-act race if two threads call it simultaneously before the instance is created.
When to use it: there's a genuine, application-wide need for exactly one instance, and that need is unlikely to change (a hardware resource handle, a truly global configuration loaded once at startup).
When to avoid it, and what to do instead: in almost every other case — construct the single instance once (at application startup / in your composition root) and pass it explicitly to whatever needs it (dependency injection, covered as Dependency Inversion in the OOP book's Chapter 10), rather than letting every consumer reach for a global getter. This preserves the "one instance" property while keeping the dependency visible and swappable in tests.
Complexity cost: deceptively low to write, expensive later — the hidden global dependency this pattern introduces tends to surface as a testing and refactoring cost much later than when the Singleton was first introduced, which is exactly why it's so commonly over-used early in a project's life.
Prototype
The problem it solves: creating a new object is expensive (costly initialization, a network call, complex computed state) but you already have a similar, fully-initialized object you could copy and adjust instead.
interface Prototype<T> {
clone(): T;
}
class GameCharacter implements Prototype<GameCharacter> {
constructor(
public name: string,
public stats: { health: number; mana: number; strength: number }, // expensive to recompute per-character
public inventory: string[]
) {}
clone(): GameCharacter {
return new GameCharacter(this.name, { ...this.stats }, [...this.inventory]); // deep copy, not shared references
}
}
const orcTemplate = new GameCharacter("Orc", { health: 100, mana: 0, strength: 15 }, ["Axe"]);
const orc2 = orcTemplate.clone();
orc2.name = "Orc Warlord";
orc2.stats.strength = 25; // safe — orcTemplate.stats is untouched, because clone() deep-copied it
import copy
class GameCharacter:
def __init__(self, name: str, stats: dict, inventory: list[str]) -> None:
self.name = name
self.stats = stats
self.inventory = inventory
def clone(self) -> "GameCharacter":
return copy.deepcopy(self) # Python's standard library does the deep-copy work directly
[IN PRACTICE] The critical detail in every Prototype implementation is deep vs. shallow copy — a shallow copy shares nested mutable objects (like stats) between the original and the clone, so mutating the clone's stats would silently corrupt the original's. This is exactly the shared-mutable-reference bug pattern warned about in the OOP book's Chapter 3 (returning a mutable reference from a getter) — clone() must deep-copy anything mutable, or the pattern introduces the very bug class it's meant to avoid.
When to use it: object creation is expensive relative to copying, and you have a representative instance ("template") to copy from and adjust — common in game object spawning, and in configuration objects cloned-then-customized per environment.
When to avoid it: object creation is cheap, or the object holds resources that genuinely can't be meaningfully copied (an open file handle, a live network connection) — cloning those either doesn't make sense or requires careful special-casing that erodes the pattern's simplicity.
Complexity cost: low mechanically (one clone() method), but easy to get subtly wrong via shallow-copy bugs — the main cost is vigilance, not code volume.
Common mistakes
- Reaching for Builder for a class with two or three simple parameters. Named/keyword arguments already solve that readability problem in most modern languages — Builder earns its place with real construction complexity or sequencing.
- Using Singleton as a convenient way to smuggle global state into a codebase, rather than because there's a genuine, deliberate one-instance requirement — the OOP book's Chapter 14 concurrency and Chapter 10 DIP chapters both apply directly here.
- Implementing
clone()with a shallow copy and being surprised when mutating a clone corrupts the original — always audit every field for whether it's a mutable reference that needs deep-copying. - Not considering dependency injection as the direct alternative to Singleton — many engineers reach for Singleton reflexively without realizing "construct once, pass explicitly" achieves the same one-instance property with none of the hidden-dependency cost.
Checklist: applying Builder, Singleton, Prototype
- Builder: only introduce it when a constructor's parameter list is genuinely error-prone or construction has real sequencing/validation — check whether your language's native named-parameter support already solves the problem first.
- Singleton: before reaching for it, ask whether "construct once at startup, inject explicitly everywhere" achieves the same guarantee without hiding the dependency — prefer that by default.
- Singleton, if used: guard the lazy-initialization check-then-act sequence against concurrent access if the application is multi-threaded.
- Prototype: audit every field in
clone()— anything mutable (arrays, objects, nested class instances) must be deep-copied, not just assigned by reference.
Discussion / interview questions
-
"When would you use a Builder instead of a constructor with default parameters?" Strong answer: When there's genuine construction complexity — many optional fields making call sites hard to read even with keyword arguments, or steps that must happen in order with validation between them — not simply "more than a couple of fields."
-
"Why is Singleton controversial, and what would you use instead in most cases?" Strong answer: It's frequently a polite name for global mutable state — hidden dependencies, hard-to-test code, and concurrency risk in naive lazy initialization. Prefer constructing the instance once at startup and passing it explicitly (dependency injection) in most cases.
-
"What's the most common bug in a Prototype implementation, and how do you avoid it?" Strong answer: A shallow copy that shares mutable nested state with the original, so mutating the clone corrupts the source — avoided by deep-copying every mutable field, or using a language facility like Python's
copy.deepcopy. -
"Is a lazily-initialized Singleton thread-safe by default?" Strong answer: No — the check-then-act sequence (check if the instance exists, then create it) is a classic race condition if two threads call
getInstance()before the instance exists; needs explicit synchronization or eager initialization to be safe.
Chapter summary / cheat sheet
- Builder: solves error-prone, many-parameter construction via chained setter calls — check if native named/keyword arguments already solve your specific case before reaching for it.
- Singleton: guarantees one instance, but is usually a worse solution than "construct once, inject explicitly" — carries hidden-dependency, testability, and (if lazy) concurrency risks.
- Prototype: clones an expensive-to-construct object instead of rebuilding it — the entire implementation lives or dies on doing a correct deep copy of mutable state.
- All three creational patterns from this chapter and the last are fundamentally about controlling and decoupling how construction happens from the code that needs the constructed object — the same theme, expressed three different ways.