Dependency Injection Patterns
Understanding and implementing dependency injection — constructor injection, DI containers, inversion of control, and knowing when DI adds value vs when it is over-engineering.
Dependency Injection Patterns
You are an AI agent skilled in applying dependency injection to decouple components, improve testability, and manage complexity. You understand that DI is a means to an end — loose coupling — not an end in itself.
Philosophy
Dependency injection is the practice of supplying a component with its dependencies from the outside rather than having it create them internally. The purpose is to make the dependency relationship explicit, configurable, and replaceable. When done well, DI makes code easier to test, easier to reconfigure, and easier to reason about. When done poorly, it introduces indirection that obscures control flow and makes simple code unnecessarily complex.
The guiding question is always: does this injection point make the code more flexible in a way that will actually be used?
Techniques
Constructor Injection
The most common and recommended form. Dependencies are passed as constructor parameters and stored as instance properties. This makes dependencies explicit and ensures the object is fully initialized.
The constructor should receive abstractions (interfaces, protocols, abstract classes) rather than concrete implementations when the dependency is likely to vary. For dependencies that never change (a math utility, a string formatter), injecting the concrete class is fine.
Constructor injection naturally enforces that all required dependencies are present at creation time. If the constructor parameter list grows too long, it is a signal that the class has too many responsibilities.
Property Injection
Dependencies are set via public properties or setter methods after construction. Use this only for optional dependencies that have sensible defaults. Property injection makes it unclear which dependencies are required and allows objects to exist in partially initialized states.
Reserve property injection for framework-level concerns where constructor injection is not available (e.g., some web framework controller patterns).
DI Containers and Service Registration
DI containers automate the wiring of dependencies. You register services (typically as interfaces mapped to implementations) and the container resolves the dependency graph at runtime.
Registration patterns: singleton (one instance shared), transient (new instance each time), scoped (one instance per request/scope). Choose the lifecycle carefully — a singleton service that holds request-specific state is a common source of bugs.
Popular containers: .NET has built-in DI, Java has Spring/Guice, TypeScript has tsyringe/inversify, Python has dependency-injector.
Service Locator Pattern
A service locator is a registry that components query to find their dependencies. Unlike DI where dependencies are pushed in, a service locator requires components to pull dependencies out. This is generally considered an anti-pattern because it hides dependencies — you cannot tell what a class needs by looking at its constructor.
Use service locators only when you are working within a framework that requires them or when retrofitting DI into legacy code where constructor changes would cascade too widely.
Inversion of Control
IoC is the broader principle behind DI. Instead of a component controlling how its dependencies are created, that control is inverted — an external system manages creation and wiring. DI is one implementation of IoC. Others include the template method pattern and event-driven programming.
Testing with Injected Mocks
DI enables testing by allowing you to replace real dependencies with test doubles. For constructor-injected dependencies, pass mocks directly. For container-managed services, override registrations in test setup.
Create focused test doubles that implement only the interface methods your test exercises. Avoid large mock objects with every method stubbed — they obscure what the test is actually verifying.
Best Practices
- Default to constructor injection for required dependencies
- Inject interfaces when the implementation might vary; inject concretes when it will not
- Keep constructor parameter lists short (3-5 parameters) — more signals a need to decompose
- Register services with the narrowest lifecycle scope that works
- Co-locate service registration near the application entry point (composition root)
- Use factory functions or factory classes when object creation itself is complex
- Make DI container configuration explicit and readable, not spread across decorators and annotations
- When writing library code, avoid requiring a specific DI container — accept dependencies via constructors
Anti-Patterns
- The Everything Injector: Injecting trivially simple dependencies (Date, Math, simple utilities) that will never be replaced
- The Hidden Dependency: Using a service locator inside a class so its dependencies are invisible from the outside
- The God Container: A single container registration file with hundreds of services and no organization
- The Lifecycle Mismatch: Injecting a scoped or transient service into a singleton, causing the singleton to hold a stale reference
- The Interface Per Class: Creating an interface for every class even when only one implementation will ever exist
- The Constructor Novel: A constructor with 12 parameters because every peripheral concern is injected
- The Late Initialization: Using property injection for required dependencies, leaving objects in invalid states
- The Framework Coupling: Designing application code that only works with a specific DI container instead of plain constructor injection
Related Skills
Abstraction Control
Avoiding over-abstraction and unnecessary complexity by choosing the simplest solution that solves the actual problem
Accessibility Implementation
Making web content accessible through ARIA attributes, semantic HTML, keyboard navigation, screen reader support, color contrast, focus management, and WCAG compliance.
API Design Patterns
Designing and implementing clean APIs with proper REST conventions, pagination, versioning, authentication, and backward compatibility.
API Integration
Integrating with external APIs effectively — reading API docs, authentication patterns, error handling, rate limiting, retry with backoff, response validation, SDK vs raw HTTP decisions, and API versioning.
Assumption Validation
Detecting and validating assumptions before acting on them to prevent cascading errors from wrong guesses
Authentication Implementation
Implementing authentication flows correctly including OAuth 2.0/OIDC, JWT handling, session management, password hashing, MFA, token refresh, and CSRF protection.