Foundation · Understand
The IoC Container
Meet the ApplicationContext: the registry that creates your objects, resolves their dependencies, and remembers every singleton it made.
Primary source: Spring Framework 6.2 Reference: Container Overview
Lesson 3 of 14
1d streakThe Architectural Question
Calling new on a service class bypasses the container entirely: its fields never populate, its proxies never attach, and lifecycle callbacks never fire.
What this guide proves
- ApplicationContext
- BeanFactory
- Inversion of Control
Inversion of Control: The foundational architectural shift
In traditional procedural or tightly coupled object-oriented architectures, high-level business services directly instantiate their own low-level dependencies (such as new SqlTaskRepository()). This hardcodes infrastructure details directly into domain logic, makes unit testing without real external systems impossible, and requires modifying source code whenever an implementation changes.
Architectural Boundary · Do Not Confuse
Traditional Tight Coupling: new TaskService(new SqlTaskRepository())
Inversion of Control: Container constructs, wires, and manages instances
IoC inverts control according to the Hollywood Principle: "Don't call us, we'll call you." The component declares what collaborators it requires (via its constructor), and an external assembler—the IoC container—manufactures, wires, and supplies those dependencies at runtime.
The Spring IoC container serves as the central registry, assembler, and lifecycle coordinator. It maintains the single source of truth for runtime singleton instances, ensures that dependencies are injected before an object executes, and wraps components in dynamic proxies when cross-cutting concerns like transactions or security are requested.
The architectural boundary
BeanFactory vs ApplicationContext: Engine vs enterprise platform
At the heart of Spring lies a fundamental architectural division between two container contracts: BeanFactory and ApplicationContext. Understanding their internal differences is crucial for diagnosing startup issues, memory consumption, and enterprise behavior.
org.springframework.beans.factory.BeanFactory is the root interface of the IoC container. It provides basic configuration and lookup facilities (getBean(), containsBean(), isSingleton(), getType()). Its primary implementation is DefaultListableBeanFactory. By default, BeanFactory uses lazy initialization: it does not instantiate or wire a bean until application code explicitly requests it via getBean(). This made it ultra-lightweight for memory-constrained environments 20 years ago, but introduces severe production risk: configuration errors, missing classes, and broken wirings remain hidden at startup and crash at runtime during customer traffic.
org.springframework.context.ApplicationContext is a complete enterprise superset that extends BeanFactory (specifically ListableBeanFactory and HierarchicalBeanFactory). By default, it uses eager pre-instantiation of singletons: during startup (refresh()), it instantiates, wires, and validates every non-lazy singleton bean. This provides a fail-fast guarantee: configuration mistakes, missing dependencies, or circular constructor references crash immediately at application startup in CI or staging, never in front of end users.
| Capability / Dimension | BeanFactory | ApplicationContext |
|---|---|---|
| Bean creation timing | Lazy by default (instantiates only on getBean()) | Eager by default (pre-instantiates all singletons at boot) |
| Configuration error detection | Deferred to runtime (crashes when invoked) | Fail-fast at startup (crashes during container refresh) |
| Internationalization (i18n) | Not supported natively | Built-in via MessageSource interface |
| Event publishing | Not supported natively | Built-in via ApplicationEventPublisher & @EventListener |
| Resource pattern loading | Basic ResourceLoader only | Ant-style path resolution (ResourcePatternResolver) |
| Environment & Profiles | Requires manual configuration | Built-in EnvironmentCapable (@Profile, PropertySources) |
| Post-processor registration | Must be manually registered in code | Automatic discovery and ordered registration of BPPs & BFPPs |
| Production recommendation | Internal engine only; do not use directly | Universal standard for Spring Framework & Spring Boot |
Use it when
Always use ApplicationContext in modern enterprise applications and Spring Boot. It provides fail-fast startup safety, AOP integration, event publishing, and declarative transaction management.
Reconsider when
Do not use the low-level BeanFactory as an application service locator. If infrastructure code genuinely needs the underlying factory, require a ConfigurableApplicationContext or ConfigurableListableBeanFactory; ApplicationContext itself does not expose getBeanFactory().
Spring configuration evolution: XML, annotations, and Java Config
Over its 20-year evolution, Spring supported three major configuration styles. While modern Spring Boot applications standardise on Java-based configuration and annotations, understanding their evolution and how Spring Boot maintains basic XML interoperability is vital for enterprise developers.
<!-- 1. HISTORICAL / LEGACY XML CONFIGURATION -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="taskRepository" class="com.example.tasks.InMemoryTaskRepository"/>
<bean id="taskService" class="com.example.tasks.TaskService">
<constructor-arg ref="taskRepository"/>
</bean>
</beans>To use legacy XML configuration inside a modern Spring Boot application, annotate any @Configuration class with @ImportResource("classpath:beans.xml"). Spring Boot automatically boots the XML reader and registers those bean definitions alongside modern annotations.
// 2. STEREOTYPE ANNOTATION CONFIGURATION (Co-located with classes)
@Repository
public class InMemoryTaskRepository implements TaskRepository {}
@Service
public class TaskService {
private final TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) { // Pure constructor injection
this.taskRepository = taskRepository;
}
}
// 3. MODERN JAVA-BASED CONFIGURATION (@Configuration & @Bean)
@Configuration(proxyBeanMethods = true)
@ImportResource("classpath:beans.xml") // Basic XML interoperability if needed
public class AppConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules(); // Ideal for 3rd-party library beans
}
}The architectural convergence
How refresh() builds the running container
Every Spring container bootstrap centers on one critical method: AbstractApplicationContext.refresh(). This method executes a deterministic, 12-step sequence that moves from raw configuration metadata to a verified, running object graph.
- 01prepareRefresh(): Records startup timestamp, initializes property sources, and validates required environment variables.
- 02obtainFreshBeanFactory(): Creates and initializes the internal DefaultListableBeanFactory engine.
- 03prepareBeanFactory(): Configures standard ClassLoader, SpEL resolver, property editors, and registers standard Aware callbacks.
- 04invokeBeanFactoryPostProcessors(): Executes BeanFactoryPostProcessors (like ConfigurationClassPostProcessor, which scans @Component classes and parses @Bean methods into BeanDefinitions before any beans are created).
- 05registerBeanPostProcessors(): Finds and registers all BeanPostProcessor definitions, sorted by @Order and PriorityOrdered.
- 06initMessageSource(): Configures internationalization and message resolution subsystem.
- 07initApplicationEventMulticaster(): Configures internal event bus for publishing and handling ApplicationEvents.
- 08onRefresh(): Subclass hook (Spring Boot starts embedded Tomcat/Jetty web server here).
- 09registerListeners(): Registers all ApplicationListener beans with the event multicaster.
- 10finishBeanFactoryInitialization(): The critical execution phase: iterates through all non-lazy singleton BeanDefinitions, resolving dependencies, running BeanPostProcessors, and caching singletons.
- 11finishRefresh(): Publishes ContextRefreshedEvent to notify listeners that the application is fully running.
Break it deliberately: The unmanaged object trap
The most common architectural bug among junior developers is assuming that adding annotations like @Service or @Transactional to a class makes all instances of that class managed by Spring, regardless of how they are instantiated.
@Service
public class TaskService {
@Autowired
private TaskRepository repository; // Field injection anti-pattern
@Transactional
public void execute(long id) {
repository.findById(id); // Will throw NullPointerException!
}
}
// In another class or test:
TaskService service = new TaskService(); // Calling 'new' bypasses the container entirely!
service.execute(42);Exception in thread "main" java.lang.NullPointerException: Cannot invoke "TaskRepository.findById(long)" because "this.repository" is null
at com.example.tasks.TaskService.execute(TaskService.java:12)
at com.example.tasks.BypassedContainerDemo.main(BypassedContainerDemo.java:18)Why calling new breaks framework guarantees
The fix is architectural: always allow Spring to instantiate and supply dependencies via constructor injection, or retrieve the managed bean directly from the ApplicationContext.
Check your understanding
Active Recall Checkpoint
Why does ApplicationContext eager pre-instantiation provide a major reliability advantage over BeanFactory lazy loading in production microservices?
Hint: Think about when configuration mistakes and missing dependencies surface to operators.
Show answer & explanation
A typical BeanFactory implementation creates non-lazy beans when requested, while an ApplicationContext typically pre-instantiates non-lazy singletons during refresh. Lazy-init settings, bean scope, and the concrete context implementation can change when failures appear, so do not present this as an unconditional interface-level rule.
Active Recall Checkpoint
How can a Spring Boot application leverage legacy XML bean configuration without abandoning modern @SpringBootApplication conventions?
Hint: Consider the configuration annotation that imports resources.
Show answer & explanation
Annotate a Java configuration class with @ImportResource("classpath:beans.xml"). Spring Boot will boot the XmlBeanDefinitionReader and merge those definitions into the standard ApplicationContext.
Runtime Architecture & Flow
Structure under inspection
The IoC Container
Part 1
ApplicationContext
Part 2
BeanFactory
Part 3
Inversion of Control
The connectors show containment and responsibility—not time or call order.
Synthesis & Production Verification
Production Acceptance Criteria
- Explain inversion of control in one sentence
- Retrieve a bean by type and by name from a manually built context
- Show that two lookups return the same singleton instance
Key Takeaway
ApplicationContext is the universal container for Spring Boot. It eagerly pre-instantiates singletons to provide fail-fast startup safety and extends BeanFactory with enterprise messaging, event publishing, and automatic post-processor detection.
Architectural & Technical Review
Core Interview Questions
Q01What is the architectural difference between BeanFactory and ApplicationContext, and why does Spring Boot use ApplicationContext?Show answer
BeanFactory is the raw IoC engine that loads beans lazily by default when getBean() is called, delaying missing dependency or syntax errors until runtime traffic. ApplicationContext extends BeanFactory and pre-instantiates all singletons eagerly at startup, providing fail-fast validation, native i18n, event multicasting, and automatic post-processor registration.
Q02How can a Spring Boot application interoperate with legacy XML bean configuration files without losing annotation features?Show answer
By annotating a Java @Configuration class with @ImportResource("classpath:beans.xml"). Spring Boot boots the XmlBeanDefinitionReader and registers the XML beans directly into the same ApplicationContext alongside component-scanned and @Bean declarations.
Q03What happens when application code directly calls "new" on a class annotated with @Service or @Transactional?Show answer
Direct instantiation bypasses the ApplicationContext entirely. The JVM allocates memory without Spring container involvement: @Autowired fields remain null, declarative annotations like @Transactional and @Cacheable fail to execute because no CGLIB/JDK proxy wraps the instance, and @PostConstruct lifecycle hooks are never invoked.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Assuming class annotations apply to instances created with "new" — Spring annotations are instructions to the container, not JVM bytecode mutations. Calling new bypasses proxy generation, dependency injection, and lifecycle callbacks completely.
- Pitfall: Relying on lazy initialization in production microservices — Enabling global lazy initialization defers startup errors until runtime customer traffic, masking missing beans or configuration typos until production outages occur.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources