Learn Spring Boot - Dependency Injection & Bean Management
Episode 4 of 24

Learn Spring Boot - Dependency Injection & Bean Management

This episode dissects the heart of Spring: beans and dependency injection. You'll learn the @Component, @Service, @Repository, and @Controller annotations, the comparison of constructor vs field vs setter injection, standard bean scopes, and lifecycle callbacks.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

In episode 2 you saw the application context storing all beans. Now we open up the details: how a class becomes a bean, how Spring injects dependencies, and how to control its lifecycle and scope.

Dependency Injection is the main reason people choose Spring. You'll write code that's easier to test, cleaner, and separated from configuration. Episode 4 is the foundation you'll use throughout the rest of the series.

The Bean Concept and Stereotype Annotations

What Is a Bean?

A bean is an object created, managed, and injected by the Spring container. You don't call new yourself — Spring does that, stores the instance in the application context, and injects it into other classes that need it.

Spring finds beans through component scan — an automatic scan of the package and sub-packages of the main class. To mark a class as a bean, use one of these annotations:

  • @Component — a generic component.
  • @Service — a class with business logic.
  • @Repository — a data access class, with automatic database exception translation.
  • @Controller / @RestController — a class that handles HTTP requests.
Bean layers in a Spring application
@Service
public class ItemService {
    // business logic
}
 
@Repository
public class ItemRepository {
    // database access
}
 
@RestController
public class ItemController {
    // HTTP endpoints
}

Technically, @Service, @Repository, and @Controller are meta-annotations of @Component. However, using the specific annotation clarifies each class's role and triggers additional behavior — for example, @Repository translates database exceptions into Spring's DataAccessException.

Constructor vs Field vs Setter Injection

The best and recommended way in Spring is constructor injection: dependencies are declared as final constructor parameters:

Constructor injection
@Service
public class ItemService {
 
    private final ItemRepository repository;
 
    public ItemService(ItemRepository repository) {
        this.repository = repository;
    }
}

Because the class has only one constructor, Spring can inject the dependency without any additional annotation. The final field ensures the dependency can't change after creation, making tests easy — you simply pass a mock through the constructor.

Field and Setter Injection

Field injection uses @Autowired directly on the field — concise but it hides dependencies and makes testing harder. Setter injection uses a setter — useful for optional dependencies. Both are allowed, but for new code, consistent constructor injection is preferred.

Standard Bean Scopes

Scope determines how long and how a bean lives:

  • singleton (default) — one instance per application context; every injection shares the same instance.
  • prototype — a new instance every time it's injected or requested.
  • request — one instance per HTTP request.
  • session — one instance per HTTP session.
  • application — one instance per ServletContext.
Setting a bean scope
@Service
@Scope("prototype")
public class AuditLogger {
    // a new instance every time it's used
}

Singleton is the right default for stateless beans, such as services and repositories. Use prototype only for objects that hold per-use state.

Custom Beans and Lifecycle Callbacks

Defining a Bean with @Bean

When a bean comes from a third-party library — for example RestClient or PasswordEncoder — you can't annotate it with @Component. The solution: declare it via a @Bean method inside a @Configuration class:

Custom bean with @Bean
@Configuration
public class AppConfig {
 
    @Bean
    public RestClient restClient() {
        return RestClient.builder()
                .baseUrl("https://api.example.com")
                .build();
    }
}

The @Bean method is called by the container at application startup, and its result is stored as a bean named after the method. This approach is also useful for setting up beans with complex initial configuration.

Lifecycle Callbacks

A bean can run logic when it's created and when it's destroyed. Spring provides two annotations: @PostConstruct for initialization after dependencies are injected, and @PreDestroy for cleanup before the container shuts the bean down.

Lifecycle callbacks
@Component
public class CacheWarmer {
 
    @PostConstruct
    public void init() {
        // prepare the cache when the application starts
    }
 
    @PreDestroy
    public void cleanup() {
        // clean up resources when the application stops
    }
}

These annotations are important for putting initialization logic that requires dependencies to already be wired — something that can't be done in the constructor.

Common Problems: Circular Dependencies and Failed Beans

Two problems you'll meet most often: circular dependency, when bean A needs B and B needs A so the container can't resolve either of them, and NoSuchBeanDefinitionException, when no bean matches the requested type. The fix for a circular dependency: refactor so it's one-directional, or use @Lazy on one of the sides. For a missing bean, check the component scan — make sure the class is inside the main class's package or a sub-package. To see the list of successfully registered beans, run the application with ./mvnw spring-boot:run and check the actuator beans endpoint.

Pattern for handling bean problems
Circular dependency -> refactor to one direction
Bean not found -> check the component scan

If either case appears, read the full stack trace — Spring usually tells you exactly which bean is problematic.

Closing

Episode 4 equipped you with the core understanding of Spring: beans as container-managed objects, stereotype annotations for marking class roles, a comparison of the three injection styles with constructor as the primary choice, singleton and prototype scopes, custom beans with @Bean, and lifecycle callbacks.

Key takeaways:

  • Beans are created and managed by the Spring container, not by manual new.
  • @Service, @Repository, and @Controller clarify each class's role.
  • Constructor injection with a final field is the recommended style.
  • The singleton scope is the default; prototype is used for per-use state.
  • Third-party libraries are declared as beans via @Bean in a @Configuration class.
  • @PostConstruct and @PreDestroy manage bean initialization and cleanup.

In the next episode, episode 5, we'll build web MVC and a basic REST API@RestController and @RequestMapping, path variables, request params, request bodies, headers, and ResponseEntity with status codes and JSON serialization. You'll start producing APIs that are genuinely useful.

Learn Spring Boot - Dependency Injection & Bean Management | Learn Spring Boot