Skip to content

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.

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)

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.

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.

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)

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)

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 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()

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.

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 collaboratorFactory Method
A family of related objects that must be created consistentlyAbstract Factory
Exactly one type, alwaysNo factory — just construct it
A need for one shared instanceA 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.

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.py
pool = GatewayPool() # created once, importable everywhere
# elsewhere
from pool import pool # the same instance, no machinery

This 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.”

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.

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.

You sayThe 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
💬 Centralize construction

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.

💬 One instance, no Singleton 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.

  • 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 Singleton class where a module-level instance would do?
  • Any Abstract Factory without a real, consistency-bound family?
  • The one-product factory. A Factory class with a single create returning one type — indirection with no payoff (Chapter 9’s named smell).
  • The Singleton reflex. A Singleton class 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/switch instead of a registry, so every new type still edits the function.
  • 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 dict of classes, a TypeScript Record of new-thunks. A class is already a callable, so no Factory class 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).