When is Abstract Factory the wrong choice, and what simpler alternatives cover the same need in a modern codebase with a dependency-injection container?
answer
- needs kinds × variants × consistency
- one variant ⇒ YAGNI
- runtime args ⇒ signature leak
- DI profile/module ≈ family wiring
- per-request/per-tenant variant ⇒ factory still wins
basics
~20 sSkip it when there is only one variant, when the products do not have to match each other, or when objects need lots of runtime arguments. A DI container binding, a configuration switch at startup, or plain constructor injection usually covers the need with less machinery.
solid answer
~50 sAbstract Factory pays for itself only when three conditions hold together: several product kinds, more than one variant of them, and a real requirement that all parts come from the same variant. Drop any one and something simpler wins. One variant → construct directly; speculative generality is the most common misuse. No consistency requirement → inject each dependency separately and let the container pick implementations. One product kind → an injected creation function or a single-method factory. Objects that need per-call runtime data → the factory signature has to carry those parameters through every variant, which is a sign the abstraction is fighting you. A DI container already does what Abstract Factory's composition root does — mapping abstract types to concrete ones from configuration — so its remaining unique value is the *family invariant*: one object supplying a matched set. If you cannot state that invariant, you probably want ordinary injection, a configuration-selected module/profile, or a plain lookup table.
go deeper
Say it is overkill when there is only one implementation, and that you would just construct the object or take it as a constructor parameter.
List the three conditions (multiple kinds, multiple variants, consistency requirement) and mention that a DI container's bindings often replace the pattern.
Compare against container profiles/modules and injected providers, explain the runtime-argument problem, and give concrete cases where the pattern still wins (per-request variants, driver SDKs, whole-subsystem test doubles).
Talk about where the variant decision belongs architecturally (process configuration vs. request context), the governance cost of a published factory interface, and how to migrate from container-wired variants to explicit factories when tenancy or multi-region requirements arrive.
### First, restate what the pattern buys Abstract Factory delivers exactly two things: 1. **Decoupling** clients from concrete classes (any factory or DI mechanism gives you this). 2. **Family consistency** — a structural guarantee that all products used together come from the same variant (this is the *unique* part). If you are only after (1), you are paying a family-shaped price for a decoupling-shaped need. ### The four situations where it is the wrong tool **A. There is one variant, and the second is hypothetical.** The classic YAGNI failure. You get two extra type hierarchies, harder navigation ("where is this actually created?"), and no benefit. Rule: introduce the abstraction when the *second* implementation actually arrives — the refactor is mechanical. **B. The products do not need to match.** If a `Logger` and a `Clock` are unrelated, bundling them into one factory invents a constraint that does not exist and couples their evolution. Inject them separately. **C. Products need runtime arguments.** Creation methods in the classic form take no parameters, so clients stay ignorant of construction. The moment you need `createOrder(customer, cart)`, every variant's signature must accept those parameters, and the factory starts leaking domain knowledge. Often what you want is a plain function/lambda injected as a creation strategy, or a Builder. **D. The "family" is really a single behavior.** If all the variants differ in is one algorithm, that is Strategy, not Abstract Factory. ### What a DI container already does A dependency-injection container (Spring, Guice, .NET's built-in container, Dagger, Koin, InversifyJS, …) is a configurable map from abstract types to concrete providers, applied at startup. That is structurally the same job as "choose the concrete factory at the composition root" — one level up. Consequences: - **Factory-per-single-product usually disappears.** Instead of `PaymentGatewayFactory`, you bind `PaymentGateway` → `StripeGateway` under one profile and `→ AdyenGateway` under another. - **Family selection can be expressed as a module/profile.** Containers support grouped bindings — Guice `Module`s, Spring `@Configuration` classes activated by profile, .NET registration extension methods. Activating one module binds *all* products of that variant at once. This achieves the family effect through configuration rather than a factory type. The guarantee is weaker (it is a wiring convention, not a type-level constraint) but the code is much smaller. - **Abstract Factory survives where creation is per-request, not per-application.** Containers wire singletons and scoped objects well; when a client must create many short-lived products *during* execution, an injected factory object (often container-generated — assisted injection, `Provider<T>`/`Func<T>` style bindings) is exactly right. ### Cheaper alternatives, ranked by weight 1. **Direct construction.** One variant, no test seam needed. 2. **Constructor injection of the products themselves.** No family requirement; the container decides the implementations. 3. **Injected creation function** (`() -> Connection`, `Provider<T>`). One product kind, needs repeated creation. 4. **Configuration-selected module/profile.** Several kinds varying together, and a wiring-level guarantee is enough. 5. **A lookup table / registry** keyed by variant name, populated at startup. Useful when variants are data-driven or plugin-supplied. 6. **Abstract Factory.** Several kinds, several variants, and the consistency invariant must be enforced in code — especially when the factory is passed around and used deep in the call graph, or the variant is chosen per request/tenant rather than per process. ### Where Abstract Factory still clearly wins - **Per-tenant or per-request variants**: a multi-tenant system that picks a storage/format/policy family per request cannot express that as static container bindings. - **Plugin/driver SDKs**: a database or cloud driver hands out a matched set of objects; the factory *is* the published entry point. - **Cross-platform toolkits**: OS-specific widget sets where mixing is catastrophic. - **Test doubles for a whole subsystem**: passing an in-memory factory swaps a coherent set of collaborators in one move. ### How to argue this in an interview State the three conditions, check them against the scenario aloud, and name the concrete simpler alternative you would use instead. Interviewers are testing whether you reach for patterns reflexively or diagnostically.
- Your DI container can activate a whole set of bindings by profile. Does that make Abstract Factory obsolete?No, but it covers the common case. Profiles bind a variant once per process; Abstract Factory can select a variant per request, per tenant, or per test, and enforces the family invariant in the type system rather than in configuration.
- A team wants a factory now because 'we might support another database later'. How do you respond?Ask what changes if you wait. Since clients already depend on abstract repository interfaces, introducing a factory later is a mechanical refactor, so build it when the second implementation exists rather than maintaining unused indirection.
- The products need runtime data to build. What do you use instead?An injected creation function or Builder that takes the runtime arguments, or container-supported assisted injection — keeping the variant-selection concern separate from the per-instance data concern.
saying these in an interview costs you the question
- Introducing Abstract Factory for a single implementation because 'it's more testable' — a plain interface plus injection already gives the test seam.
- Claiming a DI container makes the pattern obsolete in all cases, ignoring per-request and per-tenant variant selection.
- Bundling unrelated dependencies into one factory and calling the result a 'family'.
- Adding runtime parameters to every creation method and forcing all variants to accept data most of them ignore.
- Using the factory as a global/static singleton so the variant becomes ambient state — that reintroduces hidden coupling.