Inside the ApplicationContext: refresh() and Bean Creation
The ApplicationContext.refresh() method is the heart of Spring. In these twelve steps Spring reads configuration, creates bean definitions, runs post processors and creates every singleton bean. This page explains the algorithm behind getBean(), the three-level cache that solves circular references, and the extension points you can use.
BeanFactory vs ApplicationContext
| Feature | BeanFactory | ApplicationContext |
|---|---|---|
| Role | Basic container: creates and returns beans | Full container built on BeanFactory |
| Singleton creation | Lazy (on first getBean) | Eager (at startup, fails fast) |
| Events | No | Yes (ApplicationEventPublisher) |
| Message source (i18n) | No | Yes |
| Resource loading | No | Yes (classpath:, file:) |
| Post processors | Manual registration | Detected and applied automatically |
| Environment and profiles | No | Yes |
What is a BeanDefinition?
Spring does not create objects directly from your classes. First it builds a BeanDefinition for each bean: a recipe with the class name, scope, lazy flag, constructor arguments, property values, init and destroy methods, depends-on, primary flag and more. Beans are created from these recipes later. This separation lets post processors change the recipes before any object exists.
The twelve steps of refresh()
AbstractApplicationContext.refresh()
1. prepareRefresh() validate required properties, set startup time
2. obtainFreshBeanFactory() create DefaultListableBeanFactory
3. prepareBeanFactory() add standard BeanPostProcessors (ApplicationContextAware ...)
4. postProcessBeanFactory() subclasses add web scopes (request, session)
5. invokeBeanFactoryPostProcessors() ★ ConfigurationClassPostProcessor parses @Configuration:
@ComponentScan, @Import, @Bean, @ImportResource,
auto-configuration imports ► registers BeanDefinitions
then other BeanFactoryPostProcessors (property placeholders ${...})
6. registerBeanPostProcessors() create BeanPostProcessor beans (@Autowired, @Transactional proxies, ...)
7. initMessageSource() i18n
8. initApplicationEventMulticaster() event publishing
9. onRefresh() ★ creates the embedded web server (not started yet)
10. registerListeners() register ApplicationListener beans
11. finishBeanFactoryInitialization() ★ instantiate all non-lazy singleton beans
12. finishRefresh() publish ContextRefreshedEvent, ★ START the web serverHow one bean is created: getBean() workflow
getBean("orderService")
└─ doGetBean
├─ 1. singleton already created? (cache lookup, see three-level cache) yes ─► return it
├─ 2. merge BeanDefinition, check @DependsOn (create those first)
└─ 3. createBean ─► doCreateBean
├─ a. createBeanInstance choose constructor, resolve constructor args (recursive getBean)
├─ b. addSingletonFactory put an "early reference" factory into cache L3
├─ c. populateBean inject @Autowired fields and setters
│ (AutowiredAnnotationBeanPostProcessor)
├─ d. initializeBean
│ ├─ Aware callbacks (BeanNameAware, BeanFactoryAware, ApplicationContextAware)
│ ├─ BeanPostProcessor.postProcessBeforeInitialization (@PostConstruct runs here)
│ ├─ afterPropertiesSet() and custom init-method
│ └─ BeanPostProcessor.postProcessAfterInitialization ★ AOP proxy is created here
└─ e. register destroy callbacks, move bean to cache L1Three-level cache and circular dependencies
| Level | Field name | Contains |
|---|---|---|
| L1 | singletonObjects | Fully created beans (ready to use) |
| L2 | earlySingletonObjects | Early references: beans created but not yet fully initialized |
| L3 | singletonFactories | Factories (lambdas) that can produce an early reference, and apply AOP proxying if needed |
A needs B (field injection), B needs A
create A ─► instance A created, factory for A put in L3
─► populate A: needs B ─► create B ─► instance B created
populate B: needs A ─► look in cache:
L1 no, L2 no, L3 yes ─► get early reference of A, move it to L2
B finishes, goes to L1
─► A gets B, A finishes, goes to L1
With CONSTRUCTOR injection A cannot even be instantiated without B, so no early reference exists → BeanCurrentlyInCreationException.
Why L3 and not just L2? If A will be wrapped in an AOP proxy, B must receive the PROXY, not the raw object.
The factory in L3 creates the proxy at the last moment (getEarlyBeanReference).Since Spring Boot 2.6 circular references are forbidden by default (spring.main.allow-circular-references=false). Treat them as a design smell and remove the cycle.
Extension points and when each runs
| Extension point | Runs at | Can change |
|---|---|---|
| BeanDefinitionRegistryPostProcessor | Step 5, first | Register new bean definitions |
| BeanFactoryPostProcessor | Step 5, after registration | Modify existing bean definitions (no beans exist yet) |
| BeanPostProcessor | During bean creation, for every bean | Wrap or replace beans (proxies) |
| ImportSelector / ImportBeanDefinitionRegistrar | During @Configuration parsing | Decide which configurations or beans to import |
| SmartInitializingSingleton | After all singletons are created | Work that needs all beans ready |
// 1. BeanPostProcessor: measure initialization time of every bean
@Component
public class TimingBeanPostProcessor implements BeanPostProcessor {
private final Map<String, Long> starts = new ConcurrentHashMap<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
starts.put(beanName, System.nanoTime());
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
Long start = starts.remove(beanName);
if (start != null) {
long micros = (System.nanoTime() - start) / 1_000;
if (micros > 50_000) System.out.println("Slow bean init: " + beanName + " " + micros / 1000 + " ms");
}
return bean;
}
}
// 2. BeanDefinitionRegistryPostProcessor: register a bean dynamically
@Component
public class DynamicBeanRegistrar implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
BeanDefinition definition = BeanDefinitionBuilder
.genericBeanDefinition(GreetingService.class)
.addConstructorArgValue("Namaste")
.getBeanDefinition();
registry.registerBeanDefinition("dynamicGreeting", definition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { }
}
// 3. ImportSelector: import configurations based on logic
public class FeatureImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
return new String[] { "com.example.config.MetricsConfig", "com.example.config.AuditConfig" };
}
}
// usage: @Import(FeatureImportSelector.class) on a @Configuration classTip: BeanPostProcessor beans are created very early, before other beans. If a BeanPostProcessor injects normal beans, those beans are created too early and are NOT processed by all post processors (you see the log message: Bean is not eligible for getting processed by all BeanPostProcessors). Keep post processors free of injected dependencies, and declare @Bean methods that return them as static.
Common mistake: Injecting service beans into a BeanPostProcessor or BeanFactoryPostProcessor. Those beans are created early and miss proxies (for example @Transactional does not work on them). Use ObjectProvider or look the bean up lazily.
Interview questions
What happens at step 5 of refresh() in a Spring Boot application?
ConfigurationClassPostProcessor reads your @SpringBootApplication class, scans components, processes @Import and @Bean methods and the auto-configuration imports, and registers all bean definitions.
Why do singleton beans fail fast at startup?
The context creates all non-lazy singletons during refresh(), so missing beans and wrong configuration show up at startup and not in production traffic.
Where are AOP proxies created?
In BeanPostProcessor.postProcessAfterInitialization (or earlier through getEarlyBeanReference for circular references), by the auto proxy creator, which wraps the bean if an advisor matches.