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.

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.
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.@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.
The best and recommended way in Spring is constructor injection: dependencies are declared as final constructor parameters:
@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 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.
Scope determines how long and how a bean lives:
@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.
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:
@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.
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.
@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.
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.
Circular dependency -> refactor to one direction
Bean not found -> check the component scanIf either case appears, read the full stack trace — Spring usually tells you exactly which bean is problematic.
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:
new.@Service, @Repository, and @Controller clarify each class's role.@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.