Direct injection breaks down when one interface has several valid implementations and the choice depends on runtime input. The caller then needs conditional logic, manual wiring, or repeated service resolution, which makes the code harder to extend and test. A factory centralises selection, keeps the dependency tree intact, and avoids modifying consumers whenever a new implementation is added.
Why Direct Injection Breaks Once Selection Becomes Dynamic
Direct injection assumes the consumer can be wired to one correct implementation at build or configuration time. That works for a stable dependency, but it fails when the choice depends on runtime input, tenant context, feature flags, request metadata, or other conditions that are only known after the application starts handling work. At that point, the consumer is forced to become a selector, which is the wrong responsibility boundary.
The failure is architectural, not just stylistic. The consumer either starts carrying branching logic, receives a preselected implementation that may be wrong for some calls, or reaches back into a container to resolve services ad hoc. Each of those paths couples business code to selection rules and makes extension harder because every new implementation now requires touching existing callers.
That is why the problem is usually less about “dependency injection versus factory” and more about where the selection decision belongs. If the decision is invariant, direct injection is clean. If the decision varies per call, the selection logic needs its own place so the consumer can stay focused on the operation it performs.
What Actually Gets Damaged in the Code Path
When teams keep direct injection after the selection problem appears, they usually end up breaking one or more design properties at the same time. The dependency graph stops reflecting the real runtime choice, tests become awkward because they must simulate multiple branches inside the consumer, and the implementation starts to leak knowledge about concrete types that should have stayed behind an abstraction.
A factory restores that separation by making implementation choice explicit and local. The consumer asks for the needed collaborator, the factory owns the mapping from conditions to concrete implementations, and new variants can be added without rewriting the callers that already depend on the interface. For a practical implementation pattern, the OWASP Cheat Sheet Series is a useful general reference for keeping selection and control boundaries deliberate rather than implicit.
This is also where testing improves. With a factory, you can test selection policy separately from consumer behaviour, which means the consumer can be validated against a single abstraction while the factory is tested for the cases that route to each implementation. That division is what keeps extension from turning into a ripple of edits across unrelated code.
Standards & Framework Alignment
This section maps relevant standards and security frameworks to the operational risks and controls described in this guidance.
CIS Controls v8 and NIST CSF 2.0 set the governance and control requirements practitioners need to meet.
| Framework | Control / Reference | Relevance |
|---|---|---|
| CIS Controls v8 | CIS 16 — Application Software Security | Factory-based selection is a software design control that reduces fragile branching and misuse. |
| CIS 16.9 — Define and Maintain Software Standards | Consistent creation patterns prevent ad hoc resolution and duplicated selection logic. | |
| Recommendation — Separate selection logic from consumers to keep implementation changes contained and testable. Standardize object creation paths so consumers do not embed implementation choice. | ||
| NIST CSF 2.0 | PR.IP-1 — Configuration Baseline Management | Centralized factories preserve a stable dependency baseline as implementations change. |
| Recommendation — Keep selection rules in one maintained boundary instead of spreading them across callers. | ||
Practitioner Guidance
What to prioritise: use direct injection only when the consumer truly needs one implementation and the choice is stable for the lifetime of that object. If the caller starts branching on input to decide which implementation to use, that is the signal to move selection into a factory or equivalent creation boundary.
What to verify: check whether adding a new implementation would require changing consumer code, adding conditionals in the consumer, or introducing service-locator calls. If any of those are true, the design has already crossed from simple injection into manual selection, and the abstraction boundary is in the wrong place.
Common mistake: teams often keep direct injection and try to compensate with configuration-heavy constructors or if-else blocks inside business logic. That seems simpler at first, but it usually makes the code harder to extend, harder to reason about, and harder to unit test because the decision logic is now mixed with the work logic.
Practitioner takeaway: the key question is not whether dependency injection is good, but whether the consumer should be responsible for choosing the implementation. If the answer depends on runtime context, the consumer should receive an abstraction and the factory should own selection.
Risk and Threat Considerations
When selection logic is scattered across consumers, the main risk is inconsistent behaviour, especially as the number of implementations grows. Different callers may encode slightly different rules, leading to configuration drift, accidental use of the wrong implementation, or brittle hotfixes that are hard to reason about during incidents.
Failure mechanism: selection rules spread into application code, so a change in supported implementations or runtime conditions requires edits in multiple places, increasing the chance of wrong routing and regression.
Impact: the system becomes harder to extend safely, test coverage becomes less meaningful, and operational changes can introduce bugs that are difficult to trace back to a single decision point.
Related resources from NHI Mgmt Group
- What breaks when teams try to prepare for SOC 2 Type II in a sprint instead of a sustained process?
- What breaks when security teams measure resolution time using calendar time instead of technician touch time?
- What breaks when teams use the context window as a search index instead of using tools?
- What breaks when LLM access is integrated directly into each application instead of using a shared gateway?