Spring Boot
Dependency Injection (DI)
Dependency Injection is the mechanism Spring uses to implement Inversion of Control. Instead of objects creating their own dependencies, Spring injects them — through constructors, setters, or fields. Understanding the three injection styles, their trade-offs, and when to use each is fundamental to writing clean, testable Spring applications.
What Is Dependency Injection?
Dependency Injection (DI) is a specific implementation of the IoC principle. The term describes exactly what happens: instead of a class creating (constructing) its dependencies internally, they are injected (passed in) from outside.
A dependency is any object your class needs to do its work. UserService depends on UserRepository. OrderService depends on PaymentService and EmailService. These are dependencies.
Without DI, classes are responsible for creating their dependencies — which couples them tightly to specific implementations and makes testing nearly impossible. With DI, dependencies are declared and Spring provides them — the class works with whatever is injected, making it completely decoupled from how its dependencies are created or configured.
Spring supports three styles of injection: constructor injection (recommended), setter injection, and field injection. Each has specific use cases, trade-offs, and implications for testability and code quality.
Constructor Injection — The Recommended Approach
Constructor injection passes dependencies as constructor arguments. It is the recommended approach in Spring for mandatory dependencies because it makes dependencies explicit, allows fields to be final, and keeps classes testable without a Spring container.
Java
// Constructor injection — RECOMMENDED for all mandatory dependencies:
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
// @Autowired is optional when there's exactly one constructor (Spring 4.3+):
public UserService(UserRepository userRepository,
EmailService emailService,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.emailService = emailService;
this.passwordEncoder = passwordEncoder;
}
}
// Why constructor injection wins:
// 1. Fields can be FINAL — dependencies are guaranteed to be set:
private final UserRepository userRepository;
// NullPointerException is impossible — the field is set in the constructor
// or the constructor throws before the object is even created
// 2. TESTABLE WITHOUT SPRING — just call the constructor directly:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@Mock EmailService emailService;
@Mock PasswordEncoder passwordEncoder;
@InjectMocks UserService userService;
// Or manually:
UserService service = new UserService(
mock(UserRepository.class),
mock(EmailService.class),
mock(PasswordEncoder.class)
);
// No Spring context needed — pure Java unit test
}
// 3. EXPLICIT DEPENDENCIES — all deps visible in the constructor signature:
// A constructor with 8 parameters is a warning sign — class does too much
// This visibility makes over-injected classes obvious (unlike @Autowired fields)
// 4. CIRCULAR DEPENDENCY DETECTION at startup:
// A → B → A will throw BeanCurrentlyInCreationException immediately
// With field injection, circular deps silently create proxy objects
// Lombok @RequiredArgsConstructor — generates constructor automatically:
@Service
@RequiredArgsConstructor // generates constructor for all final fields
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
// Lombok generates the constructor — no boilerplate
}Setter Injection — For Optional Dependencies
Setter injection uses annotated setter methods to inject dependencies after the object is created. Use it for optional dependencies that have a reasonable default behavior when absent.
Java
// Setter injection — for OPTIONAL dependencies:
@Service
public class NotificationService {
private EmailService emailService;
private SmsService smsService;
private MetricsCollector metricsCollector;
// required=false — bean is not created if EmailService doesn't exist:
@Autowired(required = false)
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired(required = false)
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}
@Autowired(required = false)
public void setMetricsCollector(MetricsCollector metricsCollector) {
this.metricsCollector = metricsCollector;
}
public void notify(String message) {
// Gracefully handle absent optional dependencies:
if (emailService != null) {
emailService.send(message);
}
if (smsService != null) {
smsService.send(message);
}
if (metricsCollector != null) {
metricsCollector.recordNotification();
}
}
}
// Setter injection trade-offs:
// PROS:
// - Clearly marks a dependency as optional
// - Can be re-injected after construction (useful in testing)
// - Allows circular dependency resolution (Spring uses setter injection
// internally to break circular dependency cycles)
// CONS:
// - Fields cannot be final — dependency might be null
// - Object can be in an incomplete state between construction and setter call
// - Less visible than constructor parameters
// - Requires null checks throughout the class
// When to use setter injection:
// - The dependency is genuinely optional (class works without it)
// - You're integrating with a framework that requires a no-arg constructor
// - You need to reconfigure dependencies after startup (rare)Field Injection — Convenient but Avoid
Field injection places @Autowired directly on fields. It's the most concise style but has significant drawbacks for testability, immutability, and code quality. Avoid it in production code.
Java
// Field injection — @Autowired directly on the field:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository; // cannot be final
@Autowired
private CategoryService categoryService;
@Autowired
private PricingService pricingService;
}
// Why field injection is problematic:
// PROBLEM 1 — Cannot be final:
@Autowired
private ProductRepository productRepository; // no final — could be null
// PROBLEM 2 — Impossible to test without Spring context:
// To test ProductService, you MUST start a Spring context OR use reflection:
class ProductServiceTest {
@Autowired ProductService service; // needs @SpringBootTest or @ExtendWith(SpringExtension)
// Heavy, slow, loads entire context just to test one method
// The only alternative is ugly reflection:
ProductService service = new ProductService();
ReflectionTestUtils.setField(service, "productRepository", mockRepo);
// This is fragile and hides the dependency relationship
}
// PROBLEM 3 — Hidden dependencies:
// new ProductService() — looks fine, but it's missing its dependencies
// No indication at construction time that three injections are required
// Fails at runtime, not at compile time
// PROBLEM 4 — Encourages too many dependencies:
// It's easy to keep adding @Autowired fields without noticing
// The class grows without the friction that constructor parameters create
// A constructor with 10 parameters is obviously a problem
// Ten @Autowired fields are easy to ignore
// When field injection is acceptable:
// - Quick prototypes and demos (not production code)
// - Test classes themselves (@Autowired in @SpringBootTest test classes is fine)
// - Legacy codebases where refactoring isn't feasible
// In @SpringBootTest test classes — field injection is fine:
@SpringBootTest
class IntegrationTest {
@Autowired UserService userService; // acceptable in tests
@Autowired UserRepository userRepository;
}@Autowired — Injection by Type
@Autowired tells Spring to inject a dependency by type. When there is exactly one bean of the required type, injection succeeds. When there are multiple candidates, Spring needs help choosing.
Java
// Basic @Autowired — injects by type:
@Autowired
private PaymentService paymentService;
// Spring finds the single bean that implements PaymentService and injects it
// Multiple implementations — ambiguity:
@Component
public class StripePaymentService implements PaymentService { }
@Component
public class PaypalPaymentService implements PaymentService { }
// Spring finds TWO beans of type PaymentService — throws:
// NoUniqueBeanDefinitionException: expected single matching bean but found 2
// FIX 1 — @Primary: mark one as the default:
@Component
@Primary
public class StripePaymentService implements PaymentService { }
@Component
public class PaypalPaymentService implements PaymentService { }
// Now @Autowired injects StripePaymentService by default
// FIX 2 — @Qualifier: specify which bean by name:
@Autowired
@Qualifier("stripePaymentService") // bean name defaults to class name (camelCase)
private PaymentService paymentService;
// Or name the bean explicitly:
@Component("stripe")
public class StripePaymentService implements PaymentService { }
@Component("paypal")
public class PaypalPaymentService implements PaymentService { }
@Autowired
@Qualifier("stripe")
private PaymentService paymentService;
// FIX 3 — Inject ALL implementations as a collection:
@Autowired
private List<PaymentService> paymentServices;
// Contains both StripePaymentService and PaypalPaymentService
@Autowired
private Map<String, PaymentService> paymentServiceMap;
// {"stripePaymentService": StripePaymentService, "paypalPaymentService": PaypalPaymentService}
// FIX 4 — @Autowired with Optional:
@Autowired
private Optional<MetricsCollector> metricsCollector;
// Empty Optional if no MetricsCollector bean exists — no exception@Value and @ConfigurationProperties Injection
Property injection is a form of DI specific to configuration values. @Value injects individual properties. @ConfigurationProperties binds groups of properties to typed objects.
Java
// @Value — inject individual properties:
@Service
public class JwtService {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration:3600}") // default value after colon
private long expiration;
@Value("${app.jwt.issuer:https://myapp.com}")
private String issuer;
// SpEL expressions in @Value:
@Value("#{systemProperties['java.home']}")
private String javaHome;
@Value("#{'${app.allowed.origins}'.split(',')}")
private List<String> allowedOrigins;
@Value("#{T(java.lang.Math).PI}")
private double pi;
}
// @ConfigurationProperties — bind a group of properties to a class:
@ConfigurationProperties(prefix = "app.mail")
@Component
@Validated // enables Bean Validation on the properties
public class MailProperties {
@NotBlank
private String host;
@Min(1) @Max(65535)
private int port = 587;
@NotBlank
private String username;
private String password;
private boolean ssl = true;
@NotEmpty
private List<String> recipients;
// getters and setters (or use @Data from Lombok)
}
// application.properties:
// app.mail.host=smtp.gmail.com
// app.mail.port=465
// app.mail.username=myapp@gmail.com
// app.mail.password=${MAIL_PASSWORD}
// app.mail.ssl=true
// app.mail.recipients=admin@myapp.com,ops@myapp.com
// Inject and use:
@Service
public class EmailService {
private final MailProperties mail;
public EmailService(MailProperties mail) {
this.mail = mail;
}
public void send(String to, String subject, String body) {
// use mail.getHost(), mail.getPort(), etc.
}
}
// Record-based @ConfigurationProperties (Spring Boot 2.6+, Java 16+):
@ConfigurationProperties(prefix = "app.jwt")
public record JwtProperties(
@NotBlank String secret,
@Min(300) long expiration,
String issuer
) { }Handling Circular Dependencies
A circular dependency occurs when bean A depends on bean B, and bean B depends on bean A. Spring detects circular dependencies at startup and throws BeanCurrentlyInCreationException — this is a design smell that should be resolved rather than worked around.
Java
// Circular dependency — Spring detects and throws at startup:
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
}
// BeanCurrentlyInCreationException: A depends on B which depends on A
// The RIGHT fix — redesign to remove the circular dependency:
// Option 1 — Extract the shared logic into a third service:
@Service
public class SharedService {
public void sharedOperation() { }
}
@Service
public class ServiceA {
private final SharedService sharedService;
// ServiceA no longer needs ServiceB
}
@Service
public class ServiceB {
private final SharedService sharedService;
// ServiceB no longer needs ServiceA
}
// Option 2 — Use events to decouple:
@Service
public class ServiceA {
private final ApplicationEventPublisher eventPublisher;
public void doWork() {
// Instead of calling ServiceB directly, publish an event:
eventPublisher.publishEvent(new WorkCompletedEvent(this));
}
}
@Service
public class ServiceB {
@EventListener
public void onWorkCompleted(WorkCompletedEvent event) {
// React to the event — no direct dependency on ServiceA
}
}
// Option 3 — @Lazy (workaround, not ideal):
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
}
// @Lazy injects a proxy — ServiceB is created on first use
// Hides the design problem rather than solving it — use sparinglyDI Scopes — Injecting Different-Scoped Beans
Injecting a narrower-scoped bean into a wider-scoped bean causes scope mismatch — the most common scope-related bug in Spring. A singleton holding a reference to a request-scoped bean will always use the first instance created.
Java
// SCOPE MISMATCH — common bug:
@Service // singleton — lives for the entire application lifetime
public class OrderService {
@Autowired
private UserSession userSession; // request-scoped — different per request
// PROBLEM: OrderService is created once.
// It holds a reference to the FIRST UserSession ever created.
// Every request uses the same stale UserSession — WRONG.
}
// FIX 1 — Inject a scoped proxy instead of the bean directly:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession {
private String userId;
private String token;
// getters and setters
}
@Service
public class OrderService {
@Autowired
private UserSession userSession;
// userSession is a proxy — each method call on it delegates to
// the real UserSession for the CURRENT request. Works correctly.
}
// FIX 2 — Use ApplicationContext to look up the bean per request:
@Service
public class OrderService {
private final ApplicationContext context;
public OrderService(ApplicationContext context) {
this.context = context;
}
public Order createOrder(OrderRequest request) {
UserSession session = context.getBean(UserSession.class); // fresh per call
// use session...
}
}
// Scope summary:
// singleton → one instance per ApplicationContext (default)
// prototype → new instance every time it's requested
// request → one instance per HTTP request (web only)
// session → one instance per HTTP session (web only)
// application → one instance per ServletContext (web only)DI Best Practices
These are the patterns that professional Spring developers follow consistently — and the anti-patterns to recognize and avoid.
Java
// ── BEST PRACTICE 1: Always use constructor injection ────────────
// DO:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository repository; // final — guaranteed non-null
private final EmailService emailService; // visible in constructor signature
}
// DON'T:
@Service
public class UserService {
@Autowired private UserRepository repository; // not final, hidden dependency
}
// ── BEST PRACTICE 2: Program to interfaces ────────────────────────
// DO:
@Service
public class OrderService {
private final PaymentService paymentService; // interface type
// Inject StripePaymentService or PaypalPaymentService — OrderService doesn't care
}
// DON'T:
@Service
public class OrderService {
private final StripePaymentService stripePaymentService; // concrete class
// Tightly coupled — can't switch payment providers without changing OrderService
}
// ── BEST PRACTICE 3: Keep constructor parameter count low ─────────
// More than 4-5 constructor parameters = class has too many responsibilities
// DO: Split into smaller, more focused services
// Single Responsibility Principle applies here
// ── BEST PRACTICE 4: Use @Primary and @Qualifier deliberately ─────
// @Primary: "this is the default implementation — used unless overridden"
// @Qualifier: "I specifically need this exact implementation"
// Avoid both: redesign so there's only one implementation per interface
// ── BEST PRACTICE 5: Use @ConfigurationProperties over @Value ─────
// DO:
@ConfigurationProperties(prefix = "app.stripe")
public class StripeProperties {
private String apiKey;
private String webhookSecret;
private boolean testMode;
// IDE auto-complete, type safety, validation — all work
}
// DON'T (for groups of related properties):
@Value("${app.stripe.api-key}") private String apiKey;
@Value("${app.stripe.webhook-secret}") private String webhookSecret;
@Value("${app.stripe.test-mode:false}") private boolean testMode;
// No auto-complete, no validation, scattered across the class
// ── BEST PRACTICE 6: Never use DI in utility/static classes ───────
// Static utility classes don't need Spring — don't add @Component
// If a utility needs Spring beans, make it a proper @Component itself