Skip to content

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:

@RestController
public 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 objects
UserService userService = new UserService();
UserRepository userRepo = new UserRepository();
userService.setUserRepository(userRepo);

You do this:

// Spring - SPRING creates and injects objects
@Service
public 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:

@Service
public 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:

@Test
void 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:

@Repository
public class UserRepository {
public User findById(Long id) {
// database query
}
}
@Service
public 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):

@Configuration
public 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.

@Service
public 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

@Service
public 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)

@Service
public 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:

  1. Reads your annotations
  2. Creates beans
  3. Wires dependencies together
  4. 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 Ready

Bean Lifecycle

Spring beans go through a defined lifecycle:

1. Instantiation → Spring creates the object
2. Populate Properties → Spring injects dependencies
3. @PostConstruct → Your initialization code runs
4. Ready → Bean is available for use
5. @PreDestroy → Your cleanup code runs
6. Destruction → Spring removes the bean

I use @PostConstruct for startup logic:

@Component
public 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:

ScopeDescriptionUse Case
singletonOne instance per containerStateless services (default)
prototypeNew instance every timeStateful objects
requestOne instance per HTTP requestWeb applications
sessionOne instance per HTTP sessionUser 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

@Service
public class OrderService {
public void processOrder() {
PaymentProcessor processor = new PaymentProcessor(); // WRONG
// This bypasses Spring - no dependency injection
}
}

Instead:

@Service
public class OrderService {
private final PaymentProcessor processor;
public OrderService(PaymentProcessor processor) {
this.processor = processor; // RIGHT - Spring injects it
}
}

Mistake 2: Circular Dependencies

@Service
public class ServiceA {
@Autowired private ServiceB b; // A needs B
}
@Service
public 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

@Service
public 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
@Repository
public class UserRepository {
public User findById(Long id) {
// database query
return new User(id, "John Doe");
}
}
// Service layer
@Service
public 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
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

When I run this:

  1. Spring scans the package for @Component classes
  2. Creates beans for UserRepository, UserService, and UserController
  3. Injects UserRepository into UserService
  4. Injects UserService into UserController
  5. 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:

  1. A bean is just an object that Spring creates, configures, and manages
  2. Dependency injection means Spring provides the objects your class needs
  3. Constructor injection is the best approach for required dependencies
  4. Don’t use new for classes that Spring should manage
  5. 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.

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