| DI Type | Mechanism | Testability | Complexity | Best For | Languages/Frameworks |
|---|---|---|---|---|---|
| Constructor Injection | Dependencies passed via constructor parameters | Excellent | Low | Required dependencies, immutable objects | All (universal) |
| Property/Setter Injection | Dependencies assigned via public setters after construction | Good | Low | Optional dependencies, circular dep workarounds | Java/Spring, C#/.NET, Python |
| Method Injection | Dependencies passed as method parameters per-call | Excellent | Low | Per-request/transient dependencies | All (universal) |
| Interface Injection | Component implements injection interface the container calls | Good | Medium | Plugin architectures, legacy systems | Java (Avalon), less common today |
| Container-Managed | Framework auto-resolves via decorators, annotations, or config | Excellent | Medium-High | Large apps with 50+ services | Spring, NestJS, .NET, Angular, Fx |
| Manual/Pure DI | Wiring done explicitly in composition root, no container | Excellent | Low-Medium | Small-medium apps, Go idiom | Go, any language |
START
├── Is the dependency required for the object to function?
│ ├── YES → Use Constructor Injection
│ └── NO → Is the dependency optional or has a sensible default?
│ ├── YES → Use Property/Setter Injection
│ └── NO ↓
├── Does the dependency vary per method call (e.g., per-request context)?
│ ├── YES → Use Method Injection
│ └── NO ↓
├── How many services need wiring (project size)?
│ ├── < 20 services → Manual/Pure DI in a composition root
│ ├── 20-50 services → Lightweight container (Wire for Go, simple factory for TS/Python)
│ └── > 50 services → Full DI framework (Spring, NestJS, .NET DI, Uber Fx)
│ └── NO ↓
└── DEFAULT → Constructor Injection with manual wiring; add container when boilerplate becomes unmanageable
Identify the boundaries between components. Each dependency should be represented by an interface, not a concrete class. This enables swapping implementations for testing and different environments. [src1]
// TypeScript — define an interface for the dependency
interface Logger {
log(message: string): void;
}
interface UserRepository {
findById(id: string): Promise<User | null>;
}
Verify: Every class depends only on interfaces, never on concrete implementations in its constructor signature.
Pass all required dependencies through the constructor. Store them as private readonly fields. [src7]
class UserService {
constructor(
private readonly userRepo: UserRepository,
private readonly logger: Logger,
) {}
async getUser(id: string): Promise<User | null> {
this.logger.log(`Fetching user ${id}`);
return this.userRepo.findById(id);
}
}
Verify: The class has no new keywords for its dependencies — all are received from outside.
Wire all dependencies in one place — the application entry point. This is where concrete implementations are chosen and connected. [src1]
// main.ts — composition root
const logger = new ConsoleLogger();
const userRepo = new PostgresUserRepository(dbPool);
const userService = new UserService(userRepo, logger);
// Start the application with fully wired dependencies
const app = createApp(userService);
app.listen(3000);
Verify: Only the composition root references concrete classes. All other modules depend on interfaces.
Replace real implementations with test doubles. Constructor injection makes this trivial. [src7]
// user-service.test.ts
const mockRepo: UserRepository = {
findById: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
const mockLogger: Logger = { log: jest.fn() };
const service = new UserService(mockRepo, mockLogger);
const user = await service.getUser('1');
expect(user?.name).toBe('Alice');
Verify: npm test passes with no real database or external service connections.
When manual wiring becomes too verbose, introduce a framework container. Register providers and let the container resolve the dependency graph. [src4]
// NestJS example — the framework handles wiring
@Injectable()
class UserService {
constructor(
@Inject('USER_REPO') private readonly userRepo: UserRepository,
private readonly logger: LoggerService,
) {}
}
Verify: Application starts without NullInjectorError or Cannot resolve dependency errors.
// Input: NestJS module with provider registration
// Output: Auto-wired UserService with injected dependencies
import { Module, Injectable, Inject } from '@nestjs/common';
interface UserRepository {
findById(id: string): Promise<User | null>;
}
@Injectable()
class PgUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
return { id, name: 'Alice' };
}
}
@Injectable()
class UserService {
constructor(
@Inject('USER_REPO') private readonly repo: UserRepository,
) {}
}
@Module({
providers: [
{ provide: 'USER_REPO', useClass: PgUserRepository },
UserService,
],
exports: [UserService],
})
export class UserModule {}
# Input: Protocol-based interfaces, manual wiring
# Output: Testable service with swappable dependencies
from typing import Protocol
class UserRepository(Protocol):
def find_by_id(self, user_id: str) -> dict | None: ...
class PostgresUserRepository:
def __init__(self, dsn: str) -> None:
self.dsn = dsn
def find_by_id(self, user_id: str) -> dict | None:
return {"id": user_id, "name": "Alice"}
class UserService:
def __init__(self, repo: UserRepository) -> None:
self._repo = repo # injected, not created here
def get_user(self, user_id: str) -> dict | None:
return self._repo.find_by_id(user_id)
# Composition root
repo = PostgresUserRepository(dsn="postgresql://localhost/mydb")
service = UserService(repo=repo)
// Input: Spring Boot application with @Service and @Repository
// Output: Auto-wired beans managed by Spring container
public interface UserRepository {
Optional<User> findById(String id);
}
@Repository
public class JpaUserRepository implements UserRepository {
@Override
public Optional<User> findById(String id) {
return Optional.of(new User(id, "Alice"));
}
}
@Service
public class UserService {
private final UserRepository repo;
// Spring auto-injects via constructor (no @Autowired needed
// when there is only one constructor, Spring 4.3+)
public UserService(UserRepository repo) {
this.repo = repo;
}
public Optional<User> getUser(String id) {
return repo.findById(id);
}
}
// Input: Interface-based dependencies, constructor functions
// Output: Testable service with explicit wiring
package user
import "context"
// Interface (implicit in Go — no "implements" keyword)
type Repository interface {
FindByID(ctx context.Context, id string) (*User, error)
}
// Service receives its dependency via constructor function
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.FindByID(ctx, id)
}
// Input: Uber Fx module with provider functions
// Output: Auto-wired application with lifecycle management
package main
import (
"go.uber.org/fx"
"myapp/user"
"myapp/postgres"
)
func main() {
fx.New(
fx.Provide(
postgres.NewRepository, // provides user.Repository
user.NewService, // consumes user.Repository
NewHTTPHandler, // consumes *user.Service
),
fx.Invoke(StartServer),
).Run()
}
// BAD — hides dependencies, untestable, couples to container
class UserService {
getUser(id: string) {
const repo = Container.resolve<UserRepository>('UserRepo');
return repo.findById(id);
}
}
// GOOD — dependencies are visible, testable, swappable
class UserService {
constructor(private readonly repo: UserRepository) {}
getUser(id: string) {
return this.repo.findById(id);
}
}
// BAD — single container with 200+ registrations, no module boundaries
const container = new Container();
container.register('db', PostgresDB);
container.register('cache', RedisCache);
container.register('logger', WinstonLogger);
container.register('userRepo', UserRepo);
container.register('orderRepo', OrderRepo);
// ... 195 more registrations in one file
// GOOD — each module registers its own providers
@Module({
providers: [UserRepository, UserService],
exports: [UserService],
})
export class UserModule {}
@Module({
providers: [OrderRepository, OrderService],
imports: [UserModule], // explicit dependency between modules
})
export class OrderModule {}
// BAD — 8+ constructor params = class does too much (SRP violation)
public class OrderService {
public OrderService(
UserRepo userRepo, OrderRepo orderRepo, PaymentGateway payment,
EmailService email, Logger logger, MetricsService metrics,
CacheService cache, AuditService audit, NotificationService notifs
) { /* ... */ }
}
// GOOD — split into focused services with 2-4 dependencies each
public class OrderService {
public OrderService(OrderRepo orderRepo, PaymentGateway payment,
OrderNotifier notifier) { /* ... */ }
}
// OrderNotifier encapsulates email + push + audit concerns
public class OrderNotifier {
public OrderNotifier(EmailService email, NotificationService notifs,
AuditService audit) { /* ... */ }
}
// BAD — A depends on B, B depends on A → container crashes
@Injectable()
class AuthService {
constructor(private userService: UserService) {}
}
@Injectable()
class UserService {
constructor(private authService: AuthService) {}
}
// GOOD — extract shared logic into a third service
@Injectable()
class AuthService {
constructor(private tokenValidator: TokenValidator) {}
}
@Injectable()
class UserService {
constructor(private tokenValidator: TokenValidator) {}
}
// TokenValidator has no dependency on either service
@Injectable()
class TokenValidator {
validate(token: string): boolean { /* ... */ }
}
new inside classes: Bypasses DI entirely, making the class untestable. Fix: move new calls to the composition root or container registration. [src1]@Autowired on fields instead of constructors (Java/Spring): Fields can be null during construction and break immutability. Fix: use constructor injection — Spring auto-detects single constructors since 4.3. [src2]No provider for X. Fix: ensure every interface has a registered implementation in the module/container. [src4]| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building apps with multiple collaborating services that need testing | Writing a simple script or CLI tool with < 5 functions | Direct instantiation |
| You need to swap implementations (prod database vs test mock) | The project is a prototype/throwaway code with no tests | Simplest possible approach |
| Working with a framework that provides DI (Spring, NestJS, .NET) | Adding DI would require more code than the entire application | Plain function composition |
| Multiple teams work on different modules needing clear boundaries | All dependencies are pure functions with no side effects | Functional composition / higher-order functions |
| You need lifecycle management (startup/shutdown ordering) | Performance-critical hot paths where allocation overhead matters | Object pools or static dispatch |