Spring Beans Explained: What They Are and How They Work in the IoC Container
When I first started learning Spring Boot, I kept seeing the word “bean” everywhere. @Bean, @Component, “Spring manages beans”, “bean lifecycle”… I had no idea what any of it meant.
I tried reading the documentation, but it just said things like “a bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.” That didn’t help at all.
Here’s what finally made it click for me.
The Problem: Everything Just Works, But How?
I wrote my first Spring Boot REST controller:
@RestControllerpublic class UserController {
@Autowired private UserService userService;
@GetMapping("/users/{id}") public User getUser(@PathVariable Long id) { return userService.getUser(id); }}It worked. But I never created a UserService instance. I never called new UserService(). Where did it come from?
This is the “magic” of Spring beans. And understanding it is the key to understanding Spring.
What is a Bean, Really?
A Spring bean is simply an object that Spring creates and manages for you.
Instead of this:
// Traditional Java - YOU create objectsUserService userService = new UserService();UserRepository userRepo = new UserRepository();userService.setUserRepository(userRepo);You do this:
// Spring - SPRING creates and injects objects@Servicepublic class UserService { private final UserRepository userRepo;
public UserService(UserRepository userRepo) { this.userRepo = userRepo; }}Spring creates both UserService and UserRepository. It also injects the repository into the service. You don’t write any new statements.
Why Does Spring Do This?
I asked myself this question a lot. Why not just create objects myself?
Three main reasons:
1. Loose Coupling
Without Spring:
public class OrderService { private MySQLRepository repo = new MySQLRepository(); // Hardcoded to MySQL - can't change without modifying code}With Spring:
@Servicepublic class OrderService { private final OrderRepository repo;
public OrderService(OrderRepository repo) { this.repo = repo; // Spring injects whatever implementation you configure }}Now OrderService doesn’t know or care whether it’s using MySQL, PostgreSQL, or a mock for testing.
2. Easy Testing
Before Spring, testing was painful:
public class OrderService { private PaymentProcessor processor = new RealPaymentProcessor(); // Can't swap this out for a mock in tests}With Spring, I can inject a test double:
@Testvoid testOrder() { OrderService service = new OrderService(new MockPaymentProcessor()); // Easy to test without hitting real payment API}3. Centralized Configuration
All my database connections, API clients, and services are configured in one place. Spring Boot’s auto-configuration handles most of it, but I can override anything in application.properties or with @Configuration classes.
How to Create a Bean
There are two main ways I use.
Method 1: Component Annotations (Most Common)
Add a stereotype annotation to your class:
@Repositorypublic class UserRepository { public User findById(Long id) { // database query }}
@Servicepublic class UserService { private final UserRepository repo;
public UserService(UserRepository repo) { this.repo = repo; }}@Repository, @Service, and @Controller are all specializations of @Component. Spring scans your classpath and creates beans for any class with these annotations.
Method 2: @Bean Method (For Third-Party Classes)
When you can’t add annotations to a class (like a database connection pool from a library):
@Configurationpublic class DatabaseConfig {
@Bean public DataSource dataSource() { return DataSourceBuilder.create() .url("jdbc:mysql://localhost/mydb") .username("user") .password("password") .build(); }}I use this for objects from external libraries that I want Spring to manage.
How Spring Injects Dependencies
There are three ways, but I learned to prefer one.
Constructor Injection (Recommended)
@Servicepublic class OrderService { private final PaymentProcessor paymentProcessor; private final NotificationService notificationService;
public OrderService(PaymentProcessor paymentProcessor, NotificationService notificationService) { this.paymentProcessor = paymentProcessor; this.notificationService = notificationService; }}This is what I use now. Benefits:
- Dependencies are explicit and required
- Easy to test (just call the constructor with mocks)
- Fields can be
final(immutable)
Setter Injection
@Servicepublic class OrderService { private PaymentProcessor paymentProcessor;
@Autowired public void setPaymentProcessor(PaymentProcessor paymentProcessor) { this.paymentProcessor = paymentProcessor; }}I only use this for optional dependencies.
Field Injection (Don’t Use)
@Servicepublic class OrderService { @Autowired private PaymentProcessor paymentProcessor; // Hard to test, hides dependencies}I wrote code like this at first because tutorials showed it. But it’s problematic:
- Can’t create the object without Spring
- Dependencies are hidden
- Fields can’t be
final
The IoC Container
The “IoC Container” sounds fancy, but it’s just the part of Spring that:
- Reads your annotations
- Creates beans
- Wires dependencies together
- Manages the bean lifecycle
“IoC” stands for “Inversion of Control”. Instead of your code controlling object creation, Spring controls it.
Here’s what happens when my application starts:
Application Start │ ▼Spring scans for @Component classes(@Service, @Repository, @Controller, @Configuration) │ ▼Spring creates bean definitions(metadata about how to create each bean) │ ▼Spring instantiates beans in dependency order │ ▼Application ReadyBean Lifecycle
Spring beans go through a defined lifecycle:
1. Instantiation → Spring creates the object2. Populate Properties → Spring injects dependencies3. @PostConstruct → Your initialization code runs4. Ready → Bean is available for use5. @PreDestroy → Your cleanup code runs6. Destruction → Spring removes the beanI use @PostConstruct for startup logic:
@Componentpublic class DatabaseConnection {
@PostConstruct public void init() { // Runs after Spring creates the bean System.out.println("Initializing database connection..."); }
@PreDestroy public void cleanup() { // Runs before Spring destroys the bean System.out.println("Closing database connection..."); }}Bean Scopes
By default, Spring creates one instance of each bean (singleton scope). But there are other options:
| Scope | Description | Use Case |
|---|---|---|
| singleton | One instance per container | Stateless services (default) |
| prototype | New instance every time | Stateful objects |
| request | One instance per HTTP request | Web applications |
| session | One instance per HTTP session | User session data |
@Component@Scope("prototype")public class ShoppingCart { // New instance each time it's injected private List<Item> items = new ArrayList<>();}I rarely need anything other than singleton, but it’s good to know these exist.
Common Mistakes I Made
Mistake 1: Using new for Spring Beans
@Servicepublic class OrderService {
public void processOrder() { PaymentProcessor processor = new PaymentProcessor(); // WRONG // This bypasses Spring - no dependency injection }}Instead:
@Servicepublic class OrderService { private final PaymentProcessor processor;
public OrderService(PaymentProcessor processor) { this.processor = processor; // RIGHT - Spring injects it }}Mistake 2: Circular Dependencies
@Servicepublic class ServiceA { @Autowired private ServiceB b; // A needs B}
@Servicepublic class ServiceB { @Autowired private ServiceA a; // B needs A - CIRCULAR!}Spring will fail with a circular dependency error. The fix is usually to redesign so the circular dependency isn’t needed.
Mistake 3: Mutable State in Singleton Beans
@Servicepublic class UserService { private User currentUser; // DANGEROUS in singleton! // All requests share this instance}Since singleton beans are shared across all requests, mutable state causes concurrency issues. I keep singleton beans stateless.
A Complete Example
Here’s a simple Spring Boot application showing all the pieces:
// Repository layer@Repositorypublic class UserRepository { public User findById(Long id) { // database query return new User(id, "John Doe"); }}
// Service layer@Servicepublic class UserService { private final UserRepository repo;
public UserService(UserRepository repo) { this.repo = repo; }
public User getUser(Long id) { return repo.findById(id); }}
// Controller layer@RestController@RequestMapping("/users")public class UserController { private final UserService service;
public UserController(UserService service) { this.service = service; }
@GetMapping("/{id}") public User getUser(@PathVariable Long id) { return service.getUser(id); }}
// Application entry point@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}When I run this:
- Spring scans the package for
@Componentclasses - Creates beans for
UserRepository,UserService, andUserController - Injects
UserRepositoryintoUserService - Injects
UserServiceintoUserController - Starts the web server with the controller ready to handle requests
Summary
Spring beans took me a while to understand, but the concept is simple: Spring creates and manages objects for you.
The key insights:
- A bean is just an object that Spring creates, configures, and manages
- Dependency injection means Spring provides the objects your class needs
- Constructor injection is the best approach for required dependencies
- Don’t use
newfor classes that Spring should manage - Keep singleton beans stateless to avoid concurrency issues
Once I understood beans, everything else in Spring Boot made more sense. Auto-configuration, starter dependencies, even Spring Security - they’re all built on this foundation.
Related Topics
- Spring Boot Auto-Configuration - How Spring Boot configures beans automatically
- Spring Profiles - Different bean configurations for different environments
- Spring AOP - Aspect-oriented programming with Spring beans
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments