Dependency injection (DI) is the core idea behind Spring. Instead of objects creating their own collaborators, the framework hands them in. This keeps components decoupled and trivial to test.
Prefer constructor injection
Field injection with @Autowired is convenient but hides dependencies and makes
testing harder. Constructor injection makes dependencies explicit and allows
final fields.
@Service
public class OrderService {
private final PaymentGateway gateway;
private final OrderRepository repository;
public OrderService(PaymentGateway gateway, OrderRepository repository) {
this.gateway = gateway;
this.repository = repository;
}
public Order place(OrderRequest request) {
gateway.charge(request.amount());
return repository.save(new Order(request));
}
}What makes a bean
A bean is any object the Spring container manages. You declare them with
stereotype annotations or @Bean methods in a configuration class.
@Configuration
public class AppConfig {
@Bean
public PaymentGateway paymentGateway(RestClient.Builder builder) {
return new StripePaymentGateway(builder.build());
}
}Bean scopes
Most beans are singletons — one instance shared across the app. Use other scopes deliberately.
- singleton (default): one shared instance.
- prototype: a new instance every time it's requested.
- request / session: web-scoped instances tied to an HTTP lifecycle.
Why it matters for testing
Because dependencies are passed in, tests can swap real collaborators for mocks without touching the class under test.
@Test
void placesOrderAndCharges() {
var gateway = mock(PaymentGateway.class);
var repo = mock(OrderRepository.class);
var service = new OrderService(gateway, repo);
service.place(new OrderRequest(BigDecimal.TEN));
verify(gateway).charge(BigDecimal.TEN);
}Constructor injection plus mocks gives you fast, isolated unit tests with no Spring context required.