Decouple components using dependency injection for testability and flexibility. Covers constructor injection, service locators, DI containers, interface segregation, composition root, and the patterns that make code modular without overengineering.
Dependency injection is the technique of providing required dependencies to a component from external sources rather than having the component create or locate them itself. This pattern matters when building systems that must support testing, configuration changes, or modular architecture.
When a class requires dependencies at creation time, inject them through the constructor:
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
In languages without constructor injection support, pass dependencies as parameters to a factory method:
def create_user_service(user_repository, email_service):
return UserService(user_repository, email_service)
When dependencies are needed only for specific operations:
public class OrderProcessor {
public void processOrder(Order order, PaymentService paymentService) {
// Use paymentService for this operation only
}
}
Use configuration objects to provide dependencies:
services:
email-service:
type: smtp
host: smtp.example.com
port: 587
database-service:
type: postgres
connection-string: "postgresql://user:pass@db:5432/myapp"
Set dependencies through environment variables:
export DATABASE_URL=postgresql://user:pass@db:5432/myapp
export EMAIL_HOST=smtp.example.com
Register services with their dependencies in a container:
// Using a DI container like InversifyJS
container.bind(UserService).toSelf();
container.bind(UserRepository).to(UserRepositoryImpl);
container.bind(EmailService).to(EmailServiceImpl);
Use factory functions for complex dependency creation:
container.bind('DatabaseConnection').toFactory(() => {
return new DatabaseConnection(config.databaseUrl);
});
Register dependencies with different lifetimes:
// Singleton
container.bind(ServiceA).toSelf().inSingletonScope();
// Transient
container.bind(ServiceB).toSelf().inTransientScope();
// Request-scoped
container.bind(ServiceC).toSelf().inRequestScope();
Define interfaces that implementations can satisfy:
public interface UserRepository {
User findById(Long id);
void save(User user);
}
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
Support multiple implementations through interface selection:
public interface PaymentProcessor {
boolean process(Payment payment);
}
public class CreditCardProcessor implements PaymentProcessor {
public boolean process(Payment payment) { /* ... */ }
}
public class PayPalProcessor implements PaymentProcessor {
public boolean process(Payment payment) { /* ... */ }
}
Build complex dependencies from smaller ones:
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final AuditLogger auditLogger;
public UserService(UserRepository userRepository,
EmailService emailService,
AuditLogger auditLogger) {
this.userRepository = userRepository;
this.emailService = emailService;
this.auditLogger = auditLogger;
}
}
Use builders when dependency construction is complex:
public class DatabaseBuilder {
public static Database build() {
return new Database.Builder()
.withConnectionPool(config.poolSize)
.withTimeout(config.timeout)
.build();
}
}
Select dependencies based on runtime conditions:
public class PaymentService {
private final PaymentProcessor processor;
public PaymentService(String paymentMethod) {
if ("credit".equals(paymentMethod)) {
this.processor = new CreditCardProcessor();
} else if ("paypal".equals(paymentMethod)) {
this.processor = new PayPalProcessor();
} else {
throw new IllegalArgumentException("Unknown payment method");
}
}
}
Provide different implementations based on configuration:
public class EmailServiceFactory {
public static EmailService create(String type) {
switch (type) {
case "smtp":
return new SmtpEmailService();
case "sendgrid":
return new SendGridEmailService();
default:
throw new IllegalArgumentException("Unknown email service");
}
}
}
When dependencies create cycles, the container may fail with runtime errors:
Circular dependency detected in ServiceA -> ServiceB -> ServiceA
This happens when two services depend on each other directly or through a chain.
When a required dependency is not registered, the system fails at runtime:
No matching bindings found for interface UserRepository
Misconfiguring scope leads to unexpected behavior:
// Wrong: Using singleton where transient is expected
container.bind(ServiceA).toSelf().inSingletonScope();
container.bind(ServiceB).toSelf().inTransientScope();
// Result: ServiceA instances are shared, potentially causing state issues
When constructor signatures change but injection code doesn't:
// Old constructor signature
public UserService(UserRepository repository) { ... }
// New constructor signature
public UserService(UserRepository repository, EmailService emailService) { ... }
The container will fail with missing dependency errors.
Misconfigured environment variables cause runtime failures:
# Missing required variable
export DATABASE_URL= # Empty value
# Result: Database connection fails at runtime
Dynamic resolution without proper fallbacks causes failures:
// No default implementation
public class PaymentService {
public PaymentService(String paymentType) {
// If paymentType is unknown, no error until use
this.processor = createProcessor(paymentType);
}
}
Use standard formats for dependency configuration:
dependencies:
database:
type: postgres
connection-string: ${DATABASE_URL}
pool-size: 10
cache:
type: redis
host: ${REDIS_HOST}
port: ${REDIS_PORT}
Validate configuration before application startup:
public class DependencyConfigValidator {
public void validate(DependencyConfiguration config) {
if (config.database.connectionString == null) {
throw new ConfigurationException("Database connection string required");
}
}
}
Monitor dependency availability:
// Check database connection before service startup
const db = new Database(config.database);
try {
await db.connect();
// Proceed with startup
} catch (error) {
throw new StartupException("Database connection failed");
}
Handle dependencies that may not be available:
public class LoggingService {
private final Optional<AuditLogger> auditLogger;
public LoggingService(Optional<AuditLogger> auditLogger) {
this.auditLogger = auditLogger;
}
}
Wrap services with additional behavior:
public class CachingUserService implements UserService {
private final UserService userService;
private final Cache cache;
public CachingUserService(UserService userService, Cache cache) {
this.userService = userService;
this.cache = cache;
}
}
Delay dependency creation until needed:
public class ExpensiveService {
private final Supplier<DatabaseConnection> connectionSupplier;
public ExpensiveService(Supplier<DatabaseConnection> connectionSupplier) {
this.connectionSupplier = connectionSupplier;
}
public void doWork() {
var connection = connectionSupplier.get(); // Created only when needed
// Use connection
}
}
Replace real dependencies with test doubles:
@Test
public void testUserService() {
UserRepository mockRepo = mock(UserRepository.class);
EmailService mockEmail = mock(EmailService.class);
UserService service = new UserService(mockRepo, mockEmail);
// Test logic
}
Ensure tests don't share dependencies:
@BeforeEach
public void setUp() {
// Create fresh instances for each test
this.userService = new UserService(
new MockUserRepository(),
new MockEmailService()
);
}
The key to successful dependency injection is understanding when and how to provide dependencies, recognizing common failure modes, and implementing proper configuration validation.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy