Spring IoC Container & Beans
What is the Spring IoC container?
In the previous page, we saw that manually creating and connecting objects gets hard as the app grows. The Spring IoC container does this job for you.
It's the part of Spring that:
- creates your objects,
- connects them by giving each one its dependencies,
- keeps them ready for use,
- and cleans them up when the app stops.
In Spring, the container is called the ApplicationContext.
Instead of you writing all the new calls in main, you just ask the container for what you need:
ApplicationContext context = SpringApplication.run(ShopApplication.class, args);OrderService orderService = context.getBean(OrderService.class);orderService.placeOrder("asha@example.com", "Laptop");
Spring already created OrderService and everything it needs. This is Inversion of Control in action: Spring controls object creation, not your code.
What is a Spring Bean?
A Spring Bean is simply an object that the Spring container creates and manages.
1new OrderService(...) → a normal object (you manage it)2OrderService created by Spring → a bean (Spring manages it)The class is the same. The difference is who created it.
By default, Spring creates one object per bean and shares it everywhere it's needed. So if five services need NotificationService, they all get the same object. This is called singleton scope, and it solves the "sharing is manual" problem from the previous page.
How Spring creates, manages and connects objects
When your app starts, Spring goes through these steps:
- Scan your packages for classes marked as beans (with annotations like
@Service). - Read each class's constructor to see what it needs.
- Create the beans in the right order: dependencies first.
- Inject each dependency into the classes that need it.
- Store all beans in the container, ready to use.
- Clean up the beans when the app shuts down.
Spring figures out the order on its own. You never write the wiring code.
But how does Spring know which classes should be beans? You tell it with annotations.
@Component, @Service and @Autowired
@Component
@Component marks a class as a bean: "Spring, please create and manage this."
@Componentpublic class EmailNotificationService implements NotificationService {@Overridepublic void send(String to, String message) {System.out.println("Email to " + to + ": " + message);}}
@Service
@Service works exactly like @Component. It just tells readers that this class contains business logic.
@Servicepublic class OrderService {private final NotificationService notificationService;public OrderService(NotificationService notificationService) {this.notificationService = notificationService;}public void placeOrder(String customer, String product) {System.out.println("Order placed: " + product);notificationService.send(customer, "Your order for " + product + " is confirmed");}}
Spring has a few of these "labels", all of which create beans:
| Annotation | Use it for |
|---|---|
@Component | Any general class |
@Service | Business logic |
@Repository | Database access |
@Controller / @RestController | Handling web requests |
@Autowired
@Autowired tells Spring: "find a matching bean and inject it here."
The problem it solves: before annotations, Spring needed you to write every connection by hand in an XML file:
1<bean id="orderService" class="com.example.shop.OrderService">2 <constructor-arg ref="emailNotificationService"/>3</bean>With hundreds of classes, that meant hundreds of lines to write and update. @Autowired removes all of it. Spring looks at what's needed, finds a bean of that type, and connects it automatically. That's what "auto-wire" means.
@Servicepublic class OrderService {private final NotificationService notificationService;@Autowiredpublic OrderService(NotificationService notificationService) {this.notificationService = notificationService;}}
Spring sees that the constructor needs a NotificationService, finds EmailNotificationService, and passes it in.
When can you skip it? If a class has only one constructor, Spring uses it automatically, so @Autowired isn't needed. That's why our OrderService earlier worked without it.
When do you need it?
| Situation | Need @Autowired? |
|---|---|
| Only one constructor | No |
| More than one constructor | Yes, on the one Spring should use |
| Setter injection | Yes, on the setter |
| Field injection | Yes, on the field |
Why not put it on the final field instead? @Autowired on a field means field injection: Spring fills the field after the constructor runs. But Java requires a final field to be set inside the constructor, so the two can't work together. To keep the benefits of constructor injection, keep the field final and let the constructor receive the dependency.
@Configuration and @Bean
@Component and @Service only work on classes you wrote, because you have to add the annotation to the class.
But what if you need a bean from a library, like a mail client, where you can't edit the class? Or an object that needs special setup?
You create it yourself in a @Configuration class, using a @Bean method:
@Configurationpublic class MailConfig {@Beanpublic SmtpClient smtpClient() {return new SmtpClient("smtp.example.com", 587);}}
@Configurationsays: "this class defines beans."@Beansays: "the object returned by this method is a bean."
Spring calls the method once and manages the returned object like any other bean. Other classes can now receive SmtpClient through their constructors.
A @Bean method can also receive other beans as parameters:
@Beanpublic NotificationService notificationService(SmtpClient smtpClient) {return new EmailNotificationService(smtpClient);}
@Component / @Service | @Configuration + @Bean | |
|---|---|---|
| Where it goes | On your own class | On a method that returns an object |
| Who creates the object | Spring | Your method |
| Best for | Your own classes | Library classes or custom setup |
@Primary and @Qualifier
What if there are two beans of the same type?
@Componentpublic class EmailNotificationService implements NotificationService { ... }@Componentpublic class SmsNotificationService implements NotificationService { ... }
OrderService asks for a NotificationService, but there are two. Spring can't choose, so the app fails to start with an error like:
1NoUniqueBeanDefinitionException: expected single matching bean but found 2:2emailNotificationService, smsNotificationServiceNotice the names: by default, a bean's name is its class name with a lowercase first letter.
There are two ways to fix this.
@Primary: set a default
Mark one bean as the default choice:
@Primary@Componentpublic class EmailNotificationService implements NotificationService { ... }
Now, wherever a NotificationService is needed, Spring picks email unless told otherwise.
@Qualifier: choose by name
Pick a specific bean at the place where it's injected:
@Servicepublic class OrderService {private final NotificationService notificationService;public OrderService(@Qualifier("smsNotificationService") NotificationService notificationService) {this.notificationService = notificationService;}}
This OrderService gets SMS, even though email is @Primary. @Qualifier always wins over @Primary.
@Primary | @Qualifier | |
|---|---|---|
| Where it goes | On the bean | At the injection point |
| Meaning | "Use me by default" | "Use this specific one here" |
| If both are used | Loses | Wins |
Lombok note: if you use @RequiredArgsConstructor with @Qualifier on a field, Lombok won't copy the qualifier to the constructor by default. Add this to a lombok.config file in your project root:
1lombok.copyableAnnotations += org.springframework.beans.factory.annotation.QualifierImplement DI in a Spring Boot project
Let's put it all together into a working project.
1. Create the project. Go to start.spring.io, choose Maven and Java, and generate the project. No extra dependencies are needed for this example.
2. Organize the packages. Spring only scans the main class's package and the packages below it:
1src/main/java/com/example/shop/2├── ShopApplication.java3├── notification/4│ ├── NotificationService.java5│ ├── EmailNotificationService.java6│ └── SmsNotificationService.java7└── order/8 ├── OrderService.java9 └── OrderRunner.java3. The main class.
package com.example.shop;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class ShopApplication {public static void main(String[] args) {SpringApplication.run(ShopApplication.class, args);}}
@SpringBootApplication turns on component scanning, so Spring finds every @Component and @Service under com.example.shop.
4. The interface and implementations.
package com.example.shop.notification;public interface NotificationService {void send(String to, String message);}
package com.example.shop.notification;import org.springframework.context.annotation.Primary;import org.springframework.stereotype.Component;@Primary@Componentpublic class EmailNotificationService implements NotificationService {@Overridepublic void send(String to, String message) {System.out.println("Email to " + to + ": " + message);}}
package com.example.shop.notification;import org.springframework.stereotype.Component;@Componentpublic class SmsNotificationService implements NotificationService {@Overridepublic void send(String to, String message) {System.out.println("SMS to " + to + ": " + message);}}
5. The service.
package com.example.shop.order;import com.example.shop.notification.NotificationService;import org.springframework.stereotype.Service;@Servicepublic class OrderService {private final NotificationService notificationService;public OrderService(NotificationService notificationService) {this.notificationService = notificationService;}public void placeOrder(String customer, String product) {System.out.println("Order placed: " + product);notificationService.send(customer, "Your order for " + product + " is confirmed");}}
6. Run something at startup. A CommandLineRunner bean runs once after the app starts. Spring injects OrderService into it:
package com.example.shop.order;import org.springframework.boot.CommandLineRunner;import org.springframework.stereotype.Component;@Componentpublic class OrderRunner implements CommandLineRunner {private final OrderService orderService;public OrderRunner(OrderService orderService) {this.orderService = orderService;}@Overridepublic void run(String... args) {orderService.placeOrder("asha@example.com", "Laptop");}}
7. Run it.
1mvn spring-boot:runOutput:
1Order placed: Laptop2Email to asha@example.com: Your order for Laptop is confirmedSpring found all the beans, saw that OrderRunner needs OrderService and OrderService needs NotificationService, picked email because of @Primary, created everything in the right order, and connected it all.
There isn't a single new for our classes anywhere in the project. To switch to SMS, change only OrderService's constructor:
public OrderService(@Qualifier("smsNotificationService") NotificationService notificationService) {this.notificationService = notificationService;}
1Order placed: Laptop2SMS to asha@example.com: Your order for Laptop is confirmed