IoC container, @Component, @Service, @Autowired — the core of Spring.
Published September 21, 2026
Almost every class depends on others: an OrderService needs an OrderRepository to save orders and a PaymentClient to charge cards. The question is who creates those dependencies.
// Without DI: the class builds its own dependencies
public class OrderService {
private final OrderRepository repo = new MongoOrderRepository("mongodb://prod-host/shop");
private final PaymentClient payments = new StripePaymentClient(System.getenv("STRIPE_KEY"));
}
This hard-wires everything. You can't test OrderService without a real database and a real payment provider. Swapping Stripe for another provider means editing the class. Configuration details leak everywhere.
Dependency injection (DI) flips it around: a class declares what it needs, usually as constructor parameters, and something else supplies those objects:
public class OrderService {
private final OrderRepository repo;
private final PaymentClient payments;
public OrderService(OrderRepository repo, PaymentClient payments) { // "give me these"
this.repo = repo;
this.payments = payments;
}
}
The class now depends on interfaces, not on specific implementations or their configuration. In a test you pass fakes: new OrderService(new InMemoryOrderRepository(), new FakePaymentClient()). In production, Spring creates the real ones and passes them in. This handing-over of object creation to a container is called Inversion of Control (IoC).
Spring's ApplicationContext is the IoC container. At startup it:
@Component (and its specializations), plus @Bean methods in @Configuration classes.The objects it manages are called beans.
@Service // your own classes: annotate, and component scanning finds them
public class OrderService { ... }
@Configuration
public class ClientsConfig {
@Bean // classes you don't own, or that need custom construction
public PaymentClient paymentClient(PaymentProperties props) {
return new StripePaymentClient(props.apiKey(), props.timeout());
}
}
| Annotation | Meaning |
|---|---|
@Component | A generic Spring-managed class |
@Service | Business logic. Same behaviour as @Component; documents intent |
@Repository | Data access. Also enables translation of persistence exceptions into Spring's DataAccessException |
@Controller / @RestController | Web layer: request handlers |
@Configuration + @Bean | Factory methods for beans that need explicit construction |
// 1. Constructor injection — RECOMMENDED
@Service
public class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) { this.repo = repo; } // single constructor: no @Autowired needed
}
// 2. Setter injection — for genuinely optional dependencies
@Autowired(required = false)
public void setAuditLog(AuditLog auditLog) { this.auditLog = auditLog; }
// 3. Field injection — avoid
@Autowired private OrderRepository repo;
Why constructor injection is preferred:
final.Field injection hides dependencies, prevents final, and makes plain unit tests impossible without reflection or a Spring context.
If two beans implement PaymentClient, Spring can't guess which one to inject, and startup fails with NoUniqueBeanDefinitionException. Resolve it explicitly:
@Primary @Component class StripePaymentClient implements PaymentClient { } // the default choice
@Component @Qualifier("paypal") class PaypalPaymentClient implements PaymentClient { }
public CheckoutService(@Qualifier("paypal") PaymentClient client) { ... } // pick a specific one
public PaymentRouter(List<PaymentClient> all) { ... } // or inject ALL of them
public PaymentRouter(Map<String, PaymentClient> byBeanName) { ... } // … keyed by bean name
Injecting a List or Map of implementations is a clean way to build strategy or plugin patterns without if/else chains.
| Scope | Instances | Typical use |
|---|---|---|
singleton (default) | One per container | Stateless services, repositories, clients |
prototype | New one each time it's requested | Stateful, short-lived helpers |
request / session | One per HTTP request / session | Request-scoped context (web apps) |
Because the default is singleton, one instance serves all concurrent requests. So singleton beans must be stateless or thread-safe. A mutable field in a @Service that holds per-request data is a classic concurrency bug.
A subtle trap: injecting a prototype bean into a singleton gives you one prototype instance, created once, not a fresh one per use. Use ObjectProvider<T> to get a new instance when needed.
@Bean
@Profile("dev") // only in the dev profile
PaymentClient fakePayments() { return new FakePaymentClient(); }
@Bean
@ConditionalOnProperty(name = "features.sms", havingValue = "true") // only when a flag is on
SmsNotifier smsNotifier() { ... }
This is also the mechanism behind Spring Boot's auto-configuration (see What is Spring Boot?).
If A needs B and B needs A through constructors, Spring can't create either one first, and startup fails. Spring Boot disallows circular references by default since version 2.6. The right fix is almost always design: extract the shared logic into a third bean, or replace a direct call with an event. @Lazy injection works around it, but hides the smell.
Q: What's the difference between IoC and DI? A: Inversion of Control is the general principle that a framework, not your code, controls object creation and flow. Dependency Injection is the specific technique Spring uses to implement it: supplying an object's dependencies from outside instead of having it construct them.
Q: Why is constructor injection recommended over field injection?
A: It makes dependencies explicit and mandatory, allows final fields and immutability, enables plain unit tests without Spring, and surfaces circular dependencies at startup. Field injection hides dependencies and requires reflection or a container to test.
Q: What's the difference between @Component, @Service and @Repository?
A: All three register the class as a bean via component scanning. @Service and @Component behave identically, and the name documents the layer. @Repository additionally enables exception translation, converting database-specific exceptions into Spring's DataAccessException hierarchy.
Q: Are Spring beans thread-safe? A: Spring doesn't make them thread-safe. Singleton beans are shared by all threads, so they must not hold mutable per-request state in fields. Keep them stateless, or use thread-safe structures, or request-scoped beans for per-request data.
Q: How does Spring choose between two beans of the same type?
A: It fails with an ambiguity error unless you guide it. Mark one @Primary, select one with @Qualifier, match by parameter name as a fallback, or inject all candidates as a List/Map and choose at runtime.