The Spring IoC Container
Learn how the ApplicationContext reads configuration, creates beans, and manages their lifecycle from startup to shutdown.
Introduction
In Lesson 6 you learned the principle of Inversion of Control. Now let's look at the actual object responsible for implementing it in Spring: the IoC container, most commonly accessed through the ApplicationContext interface. This lesson explains what the container does, how it reads your configuration, and the lifecycle every bean passes through.
BeanFactory vs ApplicationContext
Spring actually has two related container interfaces. BeanFactory is the most basic container, providing simple bean creation and wiring. ApplicationContext is a more feature-rich sub-interface built on top of BeanFactory, adding things like event publishing, internationalization support, and easier integration with annotation-based configuration. In practice, almost every Spring application uses ApplicationContext.
| Feature | BeanFactory | ApplicationContext |
|---|---|---|
| Basic bean creation and DI | Yes | Yes |
| Annotation-based configuration support | Limited | Full support |
| Event publishing | No | Yes |
| Typical usage in real applications | Rare, low-level | Standard choice |
Creating an ApplicationContext
You already created an ApplicationContext in Lesson 1 using AnnotationConfigApplicationContext, which is the standard implementation for Java-based configuration.
import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApp { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// The container is now fully initialized: // every @Component-annotated class has been scanned, // created, and wired together. Greeter greeter = context.getBean(Greeter.class); System.out.println(greeter.greet("World")); }}Click Run to see what this code prints.
How the Container Reads Configuration
When AnnotationConfigApplicationContext starts, it processes the @Configuration class you passed in, follows any @ComponentScan instructions to discover classes annotated with @Component (and its specializations like @Service and @Repository), and builds an internal registry mapping bean names to bean definitions — metadata describing how to construct each bean, including its dependencies.
import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;
@Configuration@ComponentScan(basePackages = "com.programinds.demo")public class AppConfig {}Only after the container has resolved this registry does it actually instantiate the beans, resolving each dependency graph in the correct order so that every dependency exists before the bean that needs it is created.
The Bean Lifecycle
- Instantiation — the container creates the bean object (usually via its constructor).
- Populate properties — any remaining dependencies are injected (for setter or field injection).
- Initialization — any @PostConstruct method or InitializingBean callback runs.
- Ready for use — the bean is available via getBean() and normal application use.
- Destruction — on container shutdown, any @PreDestroy method or DisposableBean callback runs.
Lifecycle Callbacks in Code
import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import org.springframework.stereotype.Component;
@Componentpublic class ConnectionPool {
@PostConstruct public void init() { System.out.println("ConnectionPool: opening connections..."); }
@PreDestroy public void cleanup() { System.out.println("ConnectionPool: closing connections..."); }}import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApp { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// ... use the application ...
context.close(); // triggers @PreDestroy on all singleton beans }}Click Run to see what this code prints.
Common Mistakes
- Forgetting to call context.close() (or use try-with-resources) in a standalone app, so @PreDestroy callbacks never run.
- Assuming @PostConstruct runs during the constructor — it actually runs after all dependencies have been injected.
- Confusing BeanFactory and ApplicationContext and reaching for the lower-level BeanFactory unnecessarily.
Best Practices
- Use AnnotationConfigApplicationContext (or Spring Boot's auto-configured context) for all new projects.
- Put setup logic that depends on injected fields in @PostConstruct, not the constructor.
- Always close the context explicitly (or let a framework like Spring Boot manage it) so destruction callbacks fire.
Frequently Asked Questions
A bean definition is metadata describing how to create a bean (its class, dependencies, scope); a bean instance is the actual object created from that definition.
Yes, though it is uncommon. Most applications use a single ApplicationContext for the whole application lifecycle.
On modern Java (Jakarta EE 9+), it comes from the jakarta.annotation-api, which Spring Boot includes automatically; in plain Spring projects you may need to add it explicitly.
Key Takeaways
- ApplicationContext is the standard, feature-rich Spring IoC container interface used in real applications.
- The container reads @Configuration and @ComponentScan to build a registry of bean definitions before creating any beans.
- Every bean passes through instantiation, dependency injection, initialization, and eventually destruction.
- @PostConstruct and @PreDestroy let you hook into the bean lifecycle.
Summary
The ApplicationContext is the engine that makes Inversion of Control real: it reads your configuration, builds bean definitions, and manages every bean's life from creation to destruction. With the container itself explained, we can now focus squarely on Dependency Injection — the technique the container uses to wire beans together.