Dependency Injection
Design pattern that decouples components by providing their dependencies from the outside, improving testability and modularity.
Classification
- ComplexityMedium
- Impact areaTechnical
- Decision typeArchitectural
- Organizational maturityIntermediate
Technical context
Principles & goals
Use cases & scenarios
Compromises
- Excessive use leads to hard-to-follow runtime configurations
- Incorrect lifecycle binding can cause resource leaks
- Hidden dependencies when dependencies are not clearly documented
- Prefer constructor injection for required dependencies
- Document scopes and lifecycles clearly
- Avoid using Service Locator pattern as a substitute
I/O & resources
- Abstracted interfaces and contracts
- Selection of an injector or container
- Convention or configuration guidelines
- Encapsulated, testable components
- Central configuration points for implementation binding
- Documented dependency graphs
Description
Dependency injection is a design pattern that decouples components by providing their dependencies from the outside. It improves testability, modularity and reuse by separating object creation and configuration from business logic. Lifecycle management, dependency scopes and increased indirection should be considered when applying the pattern.
✔Benefits
- Increased testability by easy swap of dependencies
- Reduced coupling and clearer module boundaries
- Better reuse and interchangeability of implementations
✖Limitations
- Increased complexity due to added indirection
- Lifecycle management can become challenging
- Not every dependency is suitable for injection (e.g., trivial data access)
Trade-offs
Metrics
- Share of injected dependencies
Percentage of dependencies provided via DI instead of being created directly.
- Test coverage of isolated components
Measure of unit test coverage for components isolated via DI.
- Configuration errors per release
Number of runtime errors caused by incorrect DI bindings.
Examples & implementations
Constructor injection in a service
A service receives repository and logger dependencies via constructor, allowing unit tests with mocks.
Spring profiles for environment variants
Different beans are loaded depending on active profile to separate test and production implementations.
Policy-based injection for feature toggles
Implementations are bound based on feature flags or policies to control experimental functionality.
Implementation steps
Define interfaces and make dependencies explicit
Choose an injection approach (e.g., constructor injection)
Integrate container/framework and configure bindings
Introduce tests and migration path incrementally
⚠️ Technical debt & bottlenecks
Technical debt
- Outdated bindings that are no longer tested
- Proliferating configuration files with inconsistent bindings
- Unclear ownership of bindings across teams
Known bottlenecks
Misuse examples
- Injecting primitive values instead of configuration objects
- Injecting too many responsibilities into a constructor
- Dynamic bindings in production without testing
Typical traps
- Unclear scope definitions lead to unexpected instance lifetimes
- Late error detection due to runtime-based bindings
- Hidden side effects from injected singleton dependencies
Required skills
Architectural drivers
Constraints
- • Existing legacy APIs without interfaces hinder injection
- • Constraints from runtime environment (e.g., limited reflection)
- • Organizational conventions for dependency management must be established