Home / Spring Boot / Spring Boot Internals / Inside the ApplicationContext: refresh() and Bean Creation

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

FeatureBeanFactoryApplicationContext
RoleBasic container: creates and returns beansFull container built on BeanFactory
Singleton creationLazy (on first getBean)Eager (at startup, fails fast)
EventsNoYes (ApplicationEventPublisher)
Message source (i18n)NoYes
Resource loadingNoYes (classpath:, file:)
Post processorsManual registrationDetected and applied automatically
Environment and profilesNoYes

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 server

How 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 L1

Three-level cache and circular dependencies

LevelField nameContains
L1singletonObjectsFully created beans (ready to use)
L2earlySingletonObjectsEarly references: beans created but not yet fully initialized
L3singletonFactoriesFactories (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 pointRuns atCan change
BeanDefinitionRegistryPostProcessorStep 5, firstRegister new bean definitions
BeanFactoryPostProcessorStep 5, after registrationModify existing bean definitions (no beans exist yet)
BeanPostProcessorDuring bean creation, for every beanWrap or replace beans (proxies)
ImportSelector / ImportBeanDefinitionRegistrarDuring @Configuration parsingDecide which configurations or beans to import
SmartInitializingSingletonAfter all singletons are createdWork 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 class

Tip: 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.