10. Creating Objects: Factories and Singleton
Welcome to the phrasebook. Part II gave you the principles; this part gives you the named structures that apply them — and Chapter 8 already promised this one would arrive. There we made using a payment gateway open-closed by injecting it through an interface. But injection raises a question it doesn’t answer: who builds the gateway? This chapter is the creational family’s answer. The unifying idea: a factory puts the knowledge of what to build in one place, so adding a type is a registration, not an edit.
The Itch
Section titled “The Itch”Chapter 8 left checkout-lite injecting an IPaymentGateway, so the payment step
never has to change. Progress — but look at the code that calls it:
def checkout(method: str, amount: float) -> Receipt: if method == "card": gateway = CardGateway() elif method == "paypal": gateway = PayPalGateway() else: raise ValueError(f"unknown payment method: {method}") return gateway.charge(amount)function checkout(method: string, amount: number): Receipt { let gateway: IPaymentGateway; if (method === "card") { gateway = new CardGateway(); } else if (method === "paypal") { gateway = new PayPalGateway(); } else { throw new Error(`unknown payment method: ${method}`); } return gateway.charge(amount);}The dispatch we removed from process_payment just moved up one level — and it
breeds. The refund flow has the same if/elif. So does the admin tool. So does the
retry job. Each knows the full list of concrete gateways and how to build each one,
and a new provider means finding and editing every copy. We made use open-closed
in Chapter 8; construction is still open for modification, in three places at once.
The Concept
Section titled “The Concept”What we want is one place that owns construction:
- A single source of truth for “how do I build a gateway for this method?”
- Adding a provider is registering it, not editing call sites.
- Callers ask for a gateway by key and receive one, never naming a concrete class.
That place is a factory. In its lightest form it is not a class or a framework —
it is a function backed by a registry (the same registry idea from the Strategy
chapter, now pointed at construction): a Python dict or a TypeScript Record
mapping each key to a way of building the thing. The insight that makes it tiny:
a class is already a callable that constructs an instance, so a table whose values
are constructors is a factory.
classDiagram
class make_gateway {
+make_gateway(method) IPaymentGateway
}
class IPaymentGateway {
<<interface>>
+charge(amount) Receipt
}
class CardGateway
class PayPalGateway
make_gateway ..> IPaymentGateway : returns
IPaymentGateway <|.. CardGateway
IPaymentGateway <|.. PayPalGateway
Callers depend only on make_gateway and the interface; the concrete classes sit
behind the factory, exactly as the principle of dependency direction (Chapter 5)
wants.
Before / After
Section titled “Before / After”Before
Section titled “Before”The construction switch, copied across every call site that needs a gateway:
def checkout(method: str, amount: float) -> Receipt: if method == "card": gateway = CardGateway() elif method == "paypal": gateway = PayPalGateway() else: raise ValueError(f"unknown payment method: {method}") return gateway.charge(amount)
def refund(method: str, amount: float) -> Receipt: if method == "card": # the same switch, copied gateway = CardGateway() elif method == "paypal": gateway = PayPalGateway() else: raise ValueError(f"unknown payment method: {method}") return gateway.charge(-amount)function checkout(method: string, amount: number): Receipt { let gateway: IPaymentGateway; if (method === "card") { gateway = new CardGateway(); } else if (method === "paypal") { gateway = new PayPalGateway(); } else { throw new Error(`unknown payment method: ${method}`); } return gateway.charge(amount);}
function refund(method: string, amount: number): Receipt { let gateway: IPaymentGateway; if (method === "card") { // the same switch, copied gateway = new CardGateway(); } else if (method === "paypal") { gateway = new PayPalGateway(); } else { throw new Error(`unknown payment method: ${method}`); } return gateway.charge(-amount);}One registry owns construction; every call site asks the factory by key:
from collections.abc import Callable
_GATEWAYS: dict[str, Callable[[], IPaymentGateway]] = { "card": CardGateway, "paypal": PayPalGateway,}
def make_gateway(method: str) -> IPaymentGateway: try: return _GATEWAYS[method]() # a class is already a callable except KeyError: raise ValueError(f"unknown payment method: {method}") from None
def checkout(method: str, amount: float) -> Receipt: return make_gateway(method).charge(amount)
def refund(method: str, amount: float) -> Receipt: return make_gateway(method).charge(-amount)// Map each key to a thunk that builds the concrete gateway.const GATEWAYS: Record<string, () => IPaymentGateway> = { card: () => new CardGateway(), paypal: () => new PayPalGateway(),};
function makeGateway(method: string): IPaymentGateway { const build = GATEWAYS[method]; if (build === undefined) { throw new Error(`unknown payment method: ${method}`); // unknown -> fail fast } return build();}
function checkout(method: string, amount: number): Receipt { return makeGateway(method).charge(amount);}
function refund(method: string, amount: number): Receipt { return makeGateway(method).charge(-amount);}The construction knowledge now lives in one registry. Adding Apple Pay is one line in
that table; no call site changes. The examples/ch10/ tests prove the growth point
literally: a CryptoGateway registered with a single entry is immediately buildable
through the factory, and an unknown method fails fast with a located error
(Chapter 7).
The other two factory shapes
Section titled “The other two factory shapes”The factory function covers the overwhelming majority of cases. Two classical variants exist for when it doesn’t:
Factory Method — when a class hierarchy should each decide what to build. A base class calls an overridable method to create its collaborator, and subclasses supply the concrete type:
class Checkout(ABC): @abstractmethod def make_gateway(self) -> IPaymentGateway: ... # the factory method
def run(self, amount: float) -> Receipt: return self.make_gateway().charge(amount) # uses it without naming a type
class CardCheckout(Checkout): def make_gateway(self) -> IPaymentGateway: return CardGateway()abstract class Checkout { protected abstract makeGateway(): IPaymentGateway; // the factory method
run(amount: number): Receipt { return this.makeGateway().charge(amount); // uses it without naming a type }}
class CardCheckout extends Checkout { protected makeGateway(): IPaymentGateway { return new CardGateway(); }}Reach for it only when you already have the hierarchy for other reasons — otherwise it is a class ceremony around what a function does in one line.
Abstract Factory — when you must build a family of objects that have to stay consistent: say a provider that comes with a matched gateway, refund handler, and receipt format, and mixing a card gateway with a PayPal refund handler would be a bug. An abstract factory bundles “make the whole family” behind one interface. It is powerful and rarely needed; most “families” are one object wearing a crowd.
Choosing among them
Section titled “Choosing among them”| You have… | Use |
|---|---|
| A concrete type to pick from data (a string, config) | Factory function + registry |
| A class hierarchy where each subclass builds its own collaborator | Factory Method |
| A family of related objects that must be created consistently | Abstract Factory |
| Exactly one type, always | No factory — just construct it |
| A need for one shared instance | A module-level instance (next section) |
The default is the top row. The bottom rows earn their place only on evidence of the problem they solve — a factory with one product in its registry is the over-engineering Chapter 9 warned about, wearing a creational hat.
The Demoted Singleton
Section titled “The Demoted Singleton”Sometimes you genuinely want exactly one of something — a connection pool, a shared config. The classical answer is the Singleton pattern: a class that ensures a single instance and offers global access to it. Agents reach for it constantly, because global state feels convenient. It rarely is: a Singleton is global mutable state by another name, it hides dependencies (code that uses it doesn’t declare it), and it fights testing, because you can’t swap the one instance for a fake.
Both our languages make the whole pattern mostly unnecessary, because a module is already a singleton — evaluated once, cached, and shared by every importer. So the idiomatic “one instance” is just a module-level object:
pool = GatewayPool() # created once, importable everywhere
# elsewherefrom pool import pool # the same instance, no machineryexport const pool = new GatewayPool(); // evaluated once, importable everywhere
// elsewhereimport { pool } from "./pool"; // the same instance, no machineryThis gives you the single shared instance with none of the ceremony — no __new__ or
metaclass in Python, no private-constructor / static instance dance in TypeScript —
and, crucially, you can still inject pool as a parameter in tests to substitute a
fake. When your agent reaches for a Singleton class, that is the push: “use a
module-level instance, and inject it where it’s used.”
Language Notes
Section titled “Language Notes”Both languages reach the same factory from the same insight — a class is a callable — but each has its own escape hatches for the construction key and the lazy instance.
The registry-of-classes is the whole trick: {"card": CardGateway} plus
registry[key]() is a complete factory, because the class is the constructor. You
will sometimes see match used for this dispatch instead — fine for a fixed, closed
set, but a match statement must be edited to add a case, while a registry dict
stays open for extension. And parse the raw key into an Enum at the boundary
(Chapter 7) when the set of methods is known, so make_gateway can never be handed a
typo’d string in the first place. For the singleton case, a module-level instance is
the first reach; functools.lru_cache on a factory function is a close second when
construction is expensive and you want it lazy.
The same trick holds, with one wrinkle: a class in TypeScript is a callable too, but
you build with new, so the registry stores constructor thunks —
{ card: () => new CardGateway() } — and registry[key]?.() builds one. (A bare
class reference { card: CardGateway } works as a new-able value too, but the
thunk reads uniformly whether the entry is a class or a plain factory function.)
Because strict mode flags noUncheckedIndexedAccess, a Record lookup is
T | undefined — that undefined check is your fail-fast, exactly where the
Python version catches KeyError.
When the set of keys is genuinely closed, a discriminated union is TypeScript’s
other answer: a switch over a kind field that the compiler checks for
exhaustiveness. It is type-safe but, like Python’s match, must be edited to add a
case — choose it when the variants are fixed, the registry when they grow. For the
lazy singleton, wrap construction in a memoized getter:
let cached: Pool | undefined; const getPool = () => (cached ??= new Pool()); — the
??= is lru_cache in one operator.
When NOT to Use
Section titled “When NOT to Use”🤖 AI Collaboration
Section titled “🤖 AI Collaboration”Construction is where agents reach for ceremony fastest — a FooFactory class, a
Singleton class, an abstract factory for a single product. Most of your work
here is keeping the solution the size of the problem.
Vocabulary
Section titled “Vocabulary”| You say | The agent hears |
|---|---|
| ”Use a factory function backed by a registry” | Centralize construction; a dict / Record of constructors, not a Factory class |
| ”Adding a provider should be one registration” | The registry is the single growth point (open-closed) |
| “Use Factory Method here” | A hierarchy decides its own type via an overridable method |
| ”Don’t build an Abstract Factory without a real family” | No family of consistent objects → no abstract factory |
| ”Use a module-level instance, not a Singleton class” | One shared instance via the module, injectable for tests |
Prompt templates
Section titled “Prompt templates”This conditional chain that builds a [gateway/handler] from a string is duplicated across call sites. Replace it with a factory function backed by a registry (map the key to a constructor — a class is already a callable). Adding a type should be one registry entry. Fail fast on an unknown key. Don’t introduce a Factory class.
We need a single shared [pool/config]. Use a module-level instance, not a Singleton class, and make it injectable as a parameter so tests can pass a fake. Explain how the module already guarantees one instance.
Review checklist
Section titled “Review checklist”- Is there more than one concrete type, chosen at runtime? (else: no factory)
- Is construction centralized — one place to add a provider?
- Is the registry the growth point, not a conditional chain or a closed
match/switch? - Any
Singletonclass where a module-level instance would do? - Any Abstract Factory without a real, consistency-bound family?
Agent failure modes
Section titled “Agent failure modes”- The one-product factory. A
Factoryclass with a singlecreatereturning one type — indirection with no payoff (Chapter 9’s named smell). - The Singleton reflex. A
Singletonclass for shared state: global, hidden, untestable. Counter with the module-level instance. - Speculative Abstract Factory. A family-creator for what is really one object. Ask to see the family.
- The dispatch that isn’t open. A closed
match/switchinstead of a registry, so every new type still edits the function.
Pattern Cheat Sheet
Section titled “Pattern Cheat Sheet”Key Takeaways
Section titled “Key Takeaways”- Injection (Chapter 8) decides what to use; a factory decides what to build — and centralizes that knowledge so adding a type is a registration, not an edit.
- The lightest factory is a function plus a registry of constructors — a Python
dictof classes, a TypeScriptRecordofnew-thunks. A class is already a callable, so noFactoryclass is needed. Factory Method (a hierarchy picks its type) and Abstract Factory (a consistent family) are the rarer variants; reach for them only on evidence. - The Singleton is demoted on purpose: a module is already a singleton in both languages, so prefer a module-level instance you can inject — not a Singleton class with global reach.
- Right-size hardest here: a factory for one product, or an abstract factory without a family, is creational ceremony. When there is nothing to manage, just call the constructor.
- Glossary terms added: factory function · factory method · abstract factory · Singleton (and the module-level alternative).