Advanced Java & Spring Boot · Lesson 3 of 16 · Updated ·
Spring Core — IoC Container, Dependency Injection, Beans and Scopes
Before Spring Boot, learn the one idea that Spring is built on: you don't create your objects — the container does.
1. The problem
// Not compiled: the tightly-coupled version we want to avoid
class WelcomeService {
private EmailNotifier notifier = new EmailNotifier(); // hard-wired
}
WelcomeService is now glued to EmailNotifier. To send SMS instead, or to test without sending real emails, you must edit the class. That is tight coupling.
2. Inversion of Control (IoC) and Dependency Injection (DI)
Instead, the class declares what it needs (usually as constructor parameters), and the Spring IoC container creates the objects and passes them in. Control of object creation is "inverted" from your code to the framework.
Objects managed by the container are called beans.
3. A complete example
An interface and two implementations:
// File: course-api/src/main/java/com/skills101/demos/di/Notifier.java
package com.skills101.demos.di;
public interface Notifier {
String send(String to, String message);
}
// File: course-api/src/main/java/com/skills101/demos/di/EmailNotifier.java
package com.skills101.demos.di;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component
@Primary // the default choice when someone asks for "a Notifier"
public class EmailNotifier implements Notifier {
@Override
public String send(String to, String message) {
return "EMAIL to " + to + ": " + message;
}
}
// File: course-api/src/main/java/com/skills101/demos/di/SmsNotifier.java
package com.skills101.demos.di;
import org.springframework.stereotype.Component;
@Component("sms") // bean name "sms"
public class SmsNotifier implements Notifier {
@Override
public String send(String to, String message) {
return "SMS to " + to + ": " + message;
}
}
The class that needs them — note there is no new anywhere:
// File: course-api/src/main/java/com/skills101/demos/di/WelcomeService.java
package com.skills101.demos.di;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class WelcomeService {
private final Notifier main;
private final Notifier backup;
// Spring calls this constructor and passes the beans in — we never write "new EmailNotifier()"
public WelcomeService(Notifier main, @Qualifier("sms") Notifier backup) {
this.main = main;
this.backup = backup;
}
public void welcome(String student) {
System.out.println(main.send(student, "Welcome to Skills101!"));
System.out.println(backup.send(student, "Your login OTP is 4821"));
}
}
Start a container and use it:
// File: course-api/src/main/java/com/skills101/demos/di/DiDemo.java
package com.skills101.demos.di;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
public class DiDemo {
@Configuration
@ComponentScan // find @Component / @Service classes in this package
static class AppConfig {
}
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) { // the IoC container
WelcomeService service = context.getBean(WelcomeService.class);
service.welcome("Asha");
// beans are singletons by default: the container hands out the same object every time
System.out.println("Same object? " + (service == context.getBean(WelcomeService.class)));
System.out.println("Notifiers: " + context.getBeanNamesForType(Notifier.class).length);
}
}
}
<!-- expect: course-api/di.txt -->
EMAIL to Asha: Welcome to Skills101!
SMS to Asha: Your login OTP is 4821
Same object? true
Notifiers: 2
@Componentmarks a class as a bean.@Service,@Repositoryand@Controllerare@Componentwith a clearer meaning.- There are two
Notifierbeans, so Spring needs a rule:@Primarypicks the default,@Qualifier("sms")asks for a specific one by name. - Same object? true — beans are singletons by default: one shared instance per container.
4. Three ways to inject
| Style | Example | Use? |
|---|---|---|
| Constructor | public WelcomeService(Notifier n) | Yes — fields can be final, easy to test, fails fast if missing |
| Setter | @Autowired void setNotifier(Notifier n) | for optional dependencies |
| Field | @Autowired private Notifier n; | avoid — hidden, hard to test |
With a single constructor, @Autowired isn't even needed (as in our example).
5. @Bean methods
For classes you can't annotate (from a library), create beans in a @Configuration class:
// Not compiled: a short sketch — the real version is SecurityConfig in lesson 9
@Configuration
class AppConfig {
@Bean
PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
}
We use exactly this pattern for security in a later lesson.
6. Scopes and lifecycle
| Scope | Instances |
|---|---|
singleton (default) | one per container |
prototype | a new one every time it's requested |
request / session | one per HTTP request / user session (web apps) |
Lifecycle: create → inject dependencies → @PostConstruct method → in use → @PreDestroy method when the container closes. Because singletons are shared across threads, don't keep per-user data in fields of a singleton bean.
Common mistakes
NoUniqueBeanDefinitionException: two beans of the same type and no@Primary/@Qualifier.NoSuchBeanDefinitionException: the class isn't annotated, or is outside the scanned package.- Calling
new MyService()yourself — that object isn't a bean, so nothing gets injected into it.
Quick recap
- IoC: the container creates objects; DI: it passes their dependencies in.
@Component/@Service/@Repository/@Controllerdeclare beans;@Beanmethods too.- Prefer constructor injection; use
@Primary/@Qualifierwhen several beans fit. - Default scope is singleton.