Vasu Patel logoVasu Patel
← Patterns of Good Design
All chapters in this track
  1. 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
  2. 02Chapter 2: Patterns and SOLID: How They Relate
  3. 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
  4. 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
  5. 05Chapter 5: Structural Patterns I: Adapter & Facade
  6. 06Chapter 6: Structural Patterns II: Decorator & Proxy
  7. 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
  8. 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
  9. 09Chapter 9: Behavioral Patterns II: Observer & Mediator
  10. 10Chapter 10: Behavioral Patterns III: Command & Memento
  11. 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
  12. 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
  13. 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
  14. 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
  15. 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
  16. 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
  17. 17Chapter 17: Architectural Patterns III: The Repository Pattern
  18. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. 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.
  2. 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.
  3. Singleton, if used: guard the lazy-initialization check-then-act sequence against concurrent access if the application is multi-threaded.
  4. 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

  1. "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."

  2. "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.

  3. "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.

  4. "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.