IoC Container and Dependency Injection
Inversion of Control (IoC) means you do not create your objects with new. The Spring container creates them (called beans), keeps them and gives them to the classes that need them. Giving a bean its dependencies is called Dependency Injection (DI).
The idea
WITHOUT DI WITH DI (Spring)
class OrderService { class OrderService {
PaymentService p = new PaymentService(); private final PaymentService p;
} OrderService(PaymentService p) { this.p = p; }
tightly coupled, hard to test } Spring creates and injects PaymentServiceStereotype annotations
| Annotation | Use for | Extra |
|---|---|---|
| @Component | Any generic bean | Base annotation |
| @Service | Business logic | Only a naming meaning |
| @Repository | Database access classes | Translates database exceptions to Spring DataAccessException |
| @Controller / @RestController | Web layer | @RestController = @Controller + @ResponseBody |
| @Configuration | Classes with @Bean methods | Processed specially (proxied) |
Three ways to inject
@Service
public class OrderService {
private final PaymentService paymentService; // final = cannot be changed
// 1. Constructor injection (recommended). @Autowired is optional with one constructor
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// 2. Setter injection (for optional dependencies)
// @Autowired public void setX(X x) { this.x = x; }
// 3. Field injection (avoid: hard to test, hides dependencies)
// @Autowired private X x;Why constructor injection? The dependencies are required and visible, fields can be final (immutable), and you can create the class in a unit test with plain new and mock objects, without Spring.
Two implementations of one interface
public interface PaymentService { String pay(int amount); }
@Service
@Primary // chosen when nothing else is specified
public class UpiPaymentService implements PaymentService {
public String pay(int amount) { return "Paid " + amount + " by UPI"; }
}
@Service("card") // bean name
public class CardPaymentService implements PaymentService {
public String pay(int amount) { return "Paid " + amount + " by card"; }
}
@Service
public class CheckoutService {
private final PaymentService defaultPayment;
private final PaymentService cardPayment;
private final Map<String, PaymentService> all; // all beans by name
private final List<PaymentService> list; // all beans as a list
public CheckoutService(PaymentService defaultPayment,
@Qualifier("card") PaymentService cardPayment,
Map<String, PaymentService> all,
List<PaymentService> list) {
this.defaultPayment = defaultPayment;
this.cardPayment = cardPayment;
this.all = all;
this.list = list;
}
}Optional and lazy dependencies
@Service
public class ReportService {
private final ObjectProvider<EmailService> emailProvider;
public ReportService(ObjectProvider<EmailService> emailProvider) {
this.emailProvider = emailProvider;
}
public void send() {
emailProvider.ifAvailable(EmailService::sendReport); // no error if the bean is missing
}
}Circular dependencies
If A needs B and B needs A, Spring cannot create them with constructor injection. Since Spring Boot 2.6 circular references are not allowed by default and the application fails to start with a clear message. The correct fix is to change the design (move shared logic to a third bean). @Lazy on one parameter is only a workaround.
How Spring resolves one @Autowired dependency (internal algorithm)
DefaultListableBeanFactory.resolveDependency(type = PaymentService, name = "checkoutPayment")
1. find ALL bean names that are assignable to the type [upiPaymentService, cardPaymentService]
2. remove candidates that are not autowire-candidates or fail a @Qualifier match
3. exactly one left? ─► inject it
4. several left, pick by:
a. @Primary on one of them ─► inject it
b. @Priority (javax/jakarta.annotation.Priority) ─► highest priority
c. field name / parameter name equals a bean name ─► inject that bean (needs -parameters compiler flag, Boot adds it)
5. still ambiguous ─► NoUniqueBeanDefinitionException none found ─► NoSuchBeanDefinitionException (unless required=false / Optional)
Collections (List<T>, Map<String,T>, Set<T>) get ALL candidates, ordered by @Order / Ordered.Injection by generic type
public interface Handler<T> { void handle(T event); }
@Component class OrderHandler implements Handler<OrderPlaced> { public void handle(OrderPlaced e) { } }
@Component class UserHandler implements Handler<UserRegistered> { public void handle(UserRegistered e) { } }
@Service
class Dispatcher {
private final Handler<OrderPlaced> orderHandler; // resolves by the generic type argument
private final List<Handler<?>> all; // all handlers
Dispatcher(Handler<OrderPlaced> orderHandler, List<Handler<?>> all) { this.orderHandler = orderHandler; this.all = all; }
}Ordering, dependencies and laziness
@Component @Order(1) class ValidationStep implements PipelineStep { }
@Component @Order(2) class PricingStep implements PipelineStep { }
@Component @Order(3) class AuditStep implements PipelineStep { }
@Service
class Pipeline {
Pipeline(List<PipelineStep> steps) { /* injected in @Order sequence */ }
}
@Component
@DependsOn("databaseMigrator") // create databaseMigrator FIRST even without a direct reference
class CacheLoader { }
@Service
class ReportService {
ReportService(@Lazy HeavyClient client) { } // a lazy proxy is injected. The real bean is created at first use
}Injecting values and non-bean objects
@Service
class MailService {
private final String from;
private final ResourceLoader loader;
private final Environment env;
MailService(@Value("${app.mail.from}") String from, // property
@Value("#{systemProperties['user.name']}") String user, // SpEL expression
ResourceLoader loader, // framework objects can be injected too
Environment env) {
this.from = from; this.loader = loader; this.env = env;
}
}Injection problems and fixes
| Problem | Cause | Fix |
|---|---|---|
| NoUniqueBeanDefinitionException | Two beans of the type | @Primary, @Qualifier, inject List or Map |
| NoSuchBeanDefinitionException | Not scanned, not annotated, or condition false | Check package, annotations, profiles and @Conditional |
| Bean created twice | Class annotated @Component AND declared with @Bean, or scanned in two contexts | Keep one definition |
| Field is null | Object created with new, not by Spring | Let Spring create it and inject it |
| Prototype bean behaves like singleton | Injected into a singleton once | ObjectProvider or scoped proxy |
| Proxy class cast error | Injecting the concrete class while a JDK proxy exists | Inject the interface, or use proxy-target-class (Boot default is CGLIB) |
// Unit test WITHOUT Spring: the reason to prefer constructor injection
class CheckoutServiceTest {
@Test
void chargesWithGivenPayment() {
PaymentService fake = amount -> "paid " + amount;
CheckoutService service = new CheckoutService(fake); // plain new, no container
assertEquals("paid 10", service.checkout(10));
}
}Common mistake: Using field injection with @Autowired everywhere. The class cannot be created without Spring in a unit test and the number of dependencies is hidden. Use constructor injection.
Interview questions
What is the difference between @Component, @Service and @Repository?
All three register a bean. @Service and @Controller are for readability. @Repository additionally translates persistence exceptions.
What happens when two beans match the type?
You get NoUniqueBeanDefinitionException. Fix it with @Primary, @Qualifier, or by injecting a List or Map.
What is the ApplicationContext?
It is the IoC container. It creates beans, injects dependencies, publishes events and reads configuration.
What is the difference between @Autowired, @Resource and @Inject?
@Autowired is Spring's, matches by type first. @Resource (JSR-250) matches by name first. @Inject (JSR-330) behaves like @Autowired without a required flag. In Spring Boot code use constructor injection and skip the annotation.
Can I inject a List of all implementations in a given order?
Yes: inject List<Interface> and put @Order or implement Ordered on the beans. Map<String, Interface> gives them by bean name.