Dependency Injection in Spring Boot
Understand how Spring's IoC container manages beans, the types of dependency injection, component scanning, and bean scopes.
Dependency Injection (DI) is the core of Spring. Instead of your classes creating their own dependencies with new, Spring creates them and injects them where needed. This makes your code loosely coupled — classes depend on abstractions, not concrete implementations — which makes it easy to swap implementations, write tests, and reason about each class in isolation.
The Problem DI Solves
Without DI, a class that needs a database and a payment gateway creates them itself. That hardwires the concrete implementation into the class, making it impossible to test without a real database or a real payment network. With DI, the class declares what it needs and Spring provides the implementation — in tests you provide mocks; in production Spring wires the real things.
// Without DI — hard to test, tightly coupled
public class OrderService {
private final PaymentGateway payment = new StripePaymentGateway(); // hardcoded concrete class
private final OrderRepository repo = new MySQLOrderRepository(); // hardcoded concrete class
// To test OrderService you must also have Stripe and MySQL running
}
// With DI — loosely coupled, easily testable
public class OrderService {
private final PaymentGateway payment;
private final OrderRepository repo;
// Spring provides the implementations at runtime
public OrderService(PaymentGateway payment, OrderRepository repo) {
this.payment = payment;
this.repo = repo;
}
// In tests: pass mocks. In production: Spring passes real implementations.
}
Beans and the IoC Container
A bean is any object managed by the Spring container. Spring creates it, wires its dependencies, and manages its lifecycle. You tell Spring about beans either by annotating your own classes or by declaring them in a @Configuration class.
Registering Beans with @Component
The stereotype annotations register a bean and communicate the role of the class. Use the most specific annotation — it makes the codebase easier to navigate and unlocks layer-specific features like @Repository’s exception translation.
// @Component — generic; use for utilities and helpers that don't fit a named layer
@Component
public class EmailFormatter {
public String format(String to, String subject, String body) {
return String.format("To: %s\nSubject: %s\n\n%s", to, subject, body);
}
}
// @Service — business logic layer; makes it clear this class contains use-case logic
@Service
public class UserService {
// ...
}
// @Repository — data access layer; also translates DB exceptions to Spring's hierarchy
@Repository
public class UserRepository {
// ...
}
// @Controller / @RestController — web layer; handles HTTP requests
@RestController
public class UserController {
// ...
}
Registering Beans with @Bean (Java Config)
Use @Bean for third-party classes you can’t annotate, or for beans that need configuration before use. The method name becomes the bean name by default.
@Configuration
public class AppConfig {
// Jackson ObjectMapper configured for Java 8 date/time types
@Bean
public ObjectMapper objectMapper() {
return JsonMapper.builder()
.addModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
}
// BCrypt with cost factor 12 — strong enough for production passwords
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
// RestTemplate with sensible timeouts to avoid hanging indefinitely
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.connectTimeout(Duration.ofSeconds(5))
.readTimeout(Duration.ofSeconds(10))
.build();
}
}
Constructor Injection (Recommended)
Constructor injection is the preferred style because it makes dependencies explicit and visible, allows fields to be final (immutable after construction), and makes the class testable without Spring — just call the constructor with whatever implementations you want.
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final EmailService emailService;
// Spring injects all dependencies through the constructor.
// @Autowired is optional when there's only one constructor (Spring 4.3+).
public OrderService(OrderRepository orderRepository,
PaymentGateway paymentGateway,
EmailService emailService) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
this.emailService = emailService;
}
public Order placeOrder(Cart cart, String paymentToken) {
Order order = orderRepository.save(Order.from(cart));
paymentGateway.charge(paymentToken, order.getTotal());
emailService.sendConfirmation(order);
return order;
}
}
With Lombok, the boilerplate disappears entirely:
@Service
@RequiredArgsConstructor // generates a constructor for all final fields automatically
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final EmailService emailService;
// ...
}
Field Injection (Avoid)
Field injection with @Autowired looks convenient but has real costs: dependencies are invisible (not in the constructor signature), fields can’t be final, and testing requires Spring’s reflection infrastructure rather than plain constructor calls.
@Service
public class UserService {
@Autowired // dependency is hidden — not visible in the constructor
private UserRepository userRepository;
// Cannot make it final; to test this you need a Spring context or reflection hacks
}
Setter Injection (For Optional Dependencies)
Setter injection is appropriate when a dependency is genuinely optional — the class works correctly even when the dependency is absent. required = false tells Spring not to fail startup if no matching bean is found.
@Service
public class NotificationService {
private MetricsCollector metricsCollector;
@Autowired(required = false) // class works without MetricsCollector
public void setMetricsCollector(MetricsCollector metricsCollector) {
this.metricsCollector = metricsCollector;
}
}
Multiple Implementations — @Qualifier and @Primary
When you have multiple beans of the same type, Spring needs to know which one to inject. @Primary marks the default; @Qualifier names a specific one to use at the injection site.
public interface PaymentGateway {
void charge(String token, double amount);
}
@Component("stripe")
public class StripeGateway implements PaymentGateway { /* ... */ }
@Component("paypal")
public class PayPalGateway implements PaymentGateway { /* ... */ }
@Primary // used by default when no @Qualifier is specified
@Component
public class StripeGateway implements PaymentGateway { /* ... */ }
// Inject a specific implementation by name
@Service
public class OrderService {
private final PaymentGateway gateway;
public OrderService(@Qualifier("paypal") PaymentGateway gateway) {
this.gateway = gateway; // PayPalGateway, regardless of @Primary
}
}
Bean Scopes
Scope controls how many instances of a bean Spring creates and when. The default singleton scope is right for stateless services. Use prototype when each injection point needs its own independent instance.
// Singleton (default) — one shared instance for the entire application lifetime
@Component
@Scope("singleton")
public class AppConfig { }
// Prototype — a fresh instance every time the bean is injected or requested
@Component
@Scope("prototype")
public class ReportGenerator { }
// Request — one instance per HTTP request; requires a proxy for injection into singletons
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext { }
// Session — one instance per HTTP session; requires a proxy for injection into singletons
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession { }
@Value — Injecting Configuration
@Value injects a single configuration property from application.properties or environment variables. The ${property:default} syntax provides a fallback value if the property isn’t set, which is useful for optional config.
@Service
public class EmailService {
@Value("${app.email.from}")
private String fromAddress;
@Value("${app.email.retry-count:3}") // default value of 3 if property is absent
private int retryCount;
@Value("${app.email.enabled:true}")
private boolean enabled;
}
application.properties:
app.email.from=noreply@example.com
app.email.retry-count=5
app.email.enabled=true
@ConfigurationProperties — Typed Config Binding
For groups of related properties, @ConfigurationProperties is cleaner than multiple @Value annotations. It binds an entire prefix of properties to a class, giving you compile-time safety and IDE autocompletion for your configuration.
@ConfigurationProperties(prefix = "app.email")
@Component
public class EmailProperties {
private String from;
private int retryCount = 3; // default values in the class
private boolean enabled = true;
private Duration timeout = Duration.ofSeconds(10);
// getters and setters (or use @Data with Lombok)
}
app.email.from=noreply@example.com
app.email.retry-count=5
app.email.timeout=15s # Spring converts "15s" to Duration automatically
Bean Lifecycle
Spring calls @PostConstruct after all dependencies are injected — use it for initialisation that needs the injected values. @PreDestroy runs just before the bean is removed — use it to release resources like connections or file handles.
@Component
public class DatabasePool {
@PostConstruct // runs once, after injection is complete — safe to use injected fields here
public void init() {
System.out.println("Opening connection pool...");
}
@PreDestroy // runs when the application shuts down — release resources here
public void cleanup() {
System.out.println("Closing connection pool...");
}
}
Testing with DI
Constructor injection makes unit tests trivial — no Spring context needed, no reflection, no special test runner. Just instantiate the class with mocks.
class OrderServiceTest {
@Test
void placeOrder_chargesPaymentGateway() {
// Create mocks with Mockito
OrderRepository repo = mock(OrderRepository.class);
PaymentGateway gateway = mock(PaymentGateway.class);
EmailService email = mock(EmailService.class);
// Inject via the constructor — plain Java, no Spring required
OrderService service = new OrderService(repo, gateway, email);
when(repo.save(any())).thenReturn(new Order("ORD-1", 99.99));
service.placeOrder(testCart(), "tok_visa");
verify(gateway).charge("tok_visa", 99.99); // assert gateway was called
verify(email).sendConfirmation(any()); // assert email was sent
}
}