How do you decide how much creational indirection a system actually needs — and what signals tell you a factory or builder layer should be removed rather than added?
answer
- name the varying axis before naming the pattern
- binding time: compile / link / startup / per-call
- invariant enforcement is the strongest justification
- delete: one impl, passthrough methods, growing type switch
- DI + named args + lambdas + immutability absorbed the boilerplate
basics
~20 sAdd creational indirection only where something genuinely varies — a second implementation, a lifetime rule, or complex assembly. If a factory has one implementation, its methods just forward to one constructor, and nothing is chosen at runtime, delete it and call the constructor.
solid answer
~60 sTreat creational indirection as a budget spent on **variation points**. Justify each one with a real force: a second implementation that exists today (test doubles alone are usually not enough — constructor injection covers those), a required lifetime or instance-count rule, a family-consistency invariant that should be impossible to violate, genuinely complex or stepwise assembly, or expensive construction better served by copying. Signals to *remove*: exactly one implementation with no second on any roadmap; factory methods that are a one-line `return new X(args)` passthrough; a `create(String type)` switch that must be edited for every new type (an abstraction that never abstracted); builders for small value objects; configuration indirection so deep that no one can answer "which class actually runs in production?"; and hand-written factories that duplicate what a DI container or language features (named/default arguments, records, copy-with) already do. The governing questions are *what varies*, *when the binding happens*, and *who owns the choice* — framework extension point, deployment configuration, or per-call argument. Pick the cheapest mechanism that supports that binding time.
go deeper
Say patterns should solve a problem you actually have; a factory with one implementation that just calls one constructor is unnecessary.
Add concrete add/remove signals: a second implementation existing, complex assembly, versus passthrough methods and one-implementation factories.
Structure it around variation axis, binding time and ownership of the choice, and note how DI containers and language features cover most classic cases.
Treat creational seams as an architectural budget tied to invariants, deployment configuration and team boundaries; be explicit about deleting speculative abstraction and about which intents survive even though the 1994 boilerplate does not.
## Framing: indirection is a purchase, not a virtue Every creational pattern buys **flexibility at a specific variation point** and pays in **types, indirection, and traceability**. The senior mistake is adding it reflexively; the equally common mistake is refusing it until the code is unchangeable. The discipline is to name the force before adding the pattern. ## The four questions that decide it **1. What varies?** If nothing varies, no pattern. Name the axis explicitly: | Varying axis | Mechanism | |---|---| | how many instances | lifetime/scope policy (DI scope; Singleton only as a last resort) | | which concrete class, chosen by a subclass of the consumer | Factory Method | | which *set* of matching classes | Abstract Factory (or DI profile) | | how a complex object is assembled | Builder | | starting state, copied from an existing instance | Prototype / copy-with | **2. When must the binding happen?** Compile time (just call the constructor) → build/link time (a module or package boundary) → startup (config file, environment variable, DI profile) → per request/per call (a factory taking runtime arguments). Choose the cheapest mechanism that reaches the required binding time. Deferring later than necessary is pure cost. **3. Who owns the choice?** A framework author exposing an extension point reaches for Factory Method. An operator choosing an environment reaches for configuration/DI. A caller who knows something at the moment of the call passes an argument or uses a builder. **4. What invariant must be impossible to break?** This is the strongest justification. "A dark button can never appear next to a light scrollbar" and "a half-initialized `Request` can never be observed" are invariants the type system can enforce through Abstract Factory and Builder respectively. Indirection that encodes an invariant earns its keep; indirection that only anticipates change often does not. ## Signals to remove creational machinery - **One-implementation interface + factory.** `PaymentGatewayFactory` returning `StripeGateway`, forever. Constructor injection of the concrete type already gives testability if it implements an interface — the factory adds nothing. - **Passthrough methods.** Every factory method is `return new X(a, b)` with no decision, caching, validation, or defaulting. - **Switch-on-type factories that keep growing.** Each new type edits the factory: the abstraction failed at its one job (Open/Closed). Either register implementations in a map keyed by type (or let the DI container do it), or accept that a simple constructor call is more honest. - **Builders for tiny value objects**, especially in languages with named/default arguments or records. - **Unanswerable production questions.** If "which implementation runs in production?" requires reading three config layers and a container registry, the indirection has exceeded comprehension budget. - **Frameworks already doing it.** Hand-rolled Singletons alongside a DI container; hand-written factories duplicating container registrations; `clone()` machinery on objects that could simply be immutable. - **Abstractions built for a variation that never came.** Delete on the second year of a single implementation; you can reintroduce it in an afternoon when the second one appears. ## Signals to add it - A **second real implementation** exists or is contractually committed (a new region, vendor, tenant, protocol version). - A **strangler/migration** is planned: routing between old and new implementations at runtime. - A **construction bug class has occurred**: mismatched families, half-initialized objects, transposed positional parameters. - **Construction cost** dominates and copying a warm instance is measurably better. - **Extension by third parties** is a product requirement (plugins must supply their own products). ## Modern erosion of the classic catalogue Much of the GoF creational tooling has been absorbed: - **DI containers** replace hand-written Abstract Factories and Singletons with registrations and scopes. - **Named/default arguments, records, and data-class copy** replace most Builders and much of Prototype. - **Higher-order functions/lambdas** replace many one-method factories: pass `() -> Connection` rather than declaring a `ConnectionFactory` interface. - **Immutability** removes deep-copy hazards entirely. The *intents* survive; the boilerplate largely does not. A principal-level answer names the intent, then chooses the lightest mechanism available in the platform — and says so explicitly, rather than reciting the 1994 class diagrams. ## Organizational dimension At scale, creational seams are also a **team boundary** device: a factory interface owned by a platform team is the contract product teams implement. That justifies indirection that a single-team codebase would not need — the flexibility is bought for organizational, not technical, reasons. Conversely, factories introduced by a team that never owned a second implementation are usually just cargo cult, and removing them is a legitimate, low-risk cleanup.
- A team argues a factory is needed 'so we can mock it in tests'. Is that a sufficient justification?Usually not. If the collaborator is injected through the constructor as an interface or an overridable type, a test can pass a fake or stub directly — no factory required. A factory is justified for tests only when the object under test must create instances *during* its work (per-request, per-item), so the test needs to control that creation; then injecting a factory or a function is the right move.
- How do you safely remove an established factory layer that has only one implementation?Inline it incrementally: confirm via call-graph and configuration search that only one implementation is ever registered, replace factory calls with direct construction at the composition root first, keep the interface if it still serves as a testing or module seam, delete the factory class, and rely on the test suite plus a canary deploy. It is reversible — reintroducing a factory is a small, mechanical change if a second implementation later appears.
- When does creational indirection stop being a code-level concern and become an architectural one?When the choice is owned outside the code: per-tenant or per-region implementations, plugin ecosystems with third-party products, migration routing between an old and a new implementation, or a platform team publishing a contract that product teams implement. Then the seam is part of the system's public contract, must be versioned and documented, and its cost is justified by organizational boundaries rather than by anticipated code change.
Creational indirection is like leaving conduit in a wall for future wiring. Where you know a circuit is coming, conduit is cheap foresight; running it through every wall in the house makes the building expensive, harder to inspect, and mostly empty forever.
saying these in an interview costs you the question
- 'We might need it later' as the sole justification for a factory layer
- Adding indirection for testability when constructor injection already suffices
- Keeping a `create(String type)` switch and calling it Open/Closed compliant
- Hand-rolling factories and Singletons inside a codebase that already runs a DI container
- Judging design quality by the number of patterns present
- Refusing to delete unused abstraction because 'removing code is risky' — inlining a single-implementation factory is mechanical and reversible