Skip to main content
spring internals

The refresh() Sequence and Extension Point Timing

4 min read Chapter 3 of 78

The refresh() Sequence and Extension Point Timing

The Annotation (or Abstraction)

Engineers reach for @PostConstruct, ApplicationRunner, @EventListener(ApplicationReadyEvent.class), and SmartInitializingSingleton when they need something to happen at startup. The common belief is that these all fire “after Spring starts.” The actual timing differences between them determine whether your initialization code can depend on other beans, whether the web server is accepting requests, and whether the application has fully loaded.

The Mechanism

AbstractApplicationContext.refresh() has thirteen steps. Each extension point fires at a specific step. Getting the timing wrong means your initialization code runs before its dependencies exist, or after the application starts accepting traffic.

The full sequence, annotated with extension point firing:

StepMethodWhat fires here
1prepareRefresh()Nothing. Sets active flag, validates required properties.
2obtainFreshBeanFactory()Bean definitions loaded (XML, annotation scan, import).
3prepareBeanFactory(bf)Default beans registered: environment, systemProperties.
4postProcessBeanFactory(bf)Subclass hook. Web contexts register web-specific scopes here.
5invokeBeanFactoryPostProcessors(bf)BeanDefinitionRegistryPostProcessor, then BeanFactoryPostProcessor.
6registerBeanPostProcessors(bf)BeanPostProcessors registered (not invoked).
7initMessageSource()MessageSource bean initialized.
8initApplicationEventMulticaster()Event multicaster ready.
9onRefresh()Embedded web server created and started.
10registerListeners()ApplicationListener beans registered with multicaster.
11finishBeanFactoryInitialization(bf)All non-lazy singletons instantiated. BeanPostProcessors fire on each bean. @PostConstruct fires here. SmartInitializingSingleton.afterSingletonsInstantiated() fires after all singletons are done.
12finishRefresh()ContextRefreshedEvent published. Lifecycle processors started.

After refresh() returns, SpringApplication fires ApplicationStartedEvent, calls ApplicationRunner and CommandLineRunner beans, then fires ApplicationReadyEvent.

The Debuggable Demonstration

This component proves the ordering by printing timestamps at each hook:

// CORRECT: Demonstrates exact firing order of all startup hooks
@Component
public class StartupTimingProbe implements
        BeanPostProcessor,
        SmartInitializingSingleton,
        ApplicationRunner,
        ApplicationListener<ContextRefreshedEvent> {

    private static final Logger log = LoggerFactory.getLogger(StartupTimingProbe.class);

    @PostConstruct
    public void postConstruct() {
        log.info("[1] @PostConstruct fired - inside finishBeanFactoryInitialization");
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (beanName.equals("orderService")) {
            log.info("[2] BeanPostProcessor.postProcessAfterInitialization for orderService");
        }
        return bean;
    }

    @Override
    public void afterSingletonsInstantiated() {
        log.info("[3] SmartInitializingSingleton - all singletons created");
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("[4] ContextRefreshedEvent - refresh() finishing");
    }

    @Override
    public void run(ApplicationArguments args) {
        log.info("[5] ApplicationRunner - after refresh(), before ApplicationReadyEvent");
    }
}

Output:

[1] @PostConstruct fired - inside finishBeanFactoryInitialization
[2] BeanPostProcessor.postProcessAfterInitialization for orderService
[3] SmartInitializingSingleton - all singletons created
[4] ContextRefreshedEvent - refresh() finishing
[5] ApplicationRunner - after refresh(), before ApplicationReadyEvent

The ordering is absolute. @PostConstruct fires as part of individual bean initialization. SmartInitializingSingleton fires after every singleton is done. ContextRefreshedEvent fires at the end of refresh(). ApplicationRunner fires after refresh() returns.

The Failure Mode

The SaaS backend needs to pre-load tenant configurations from the database at startup. The team uses @PostConstruct:

// BROKEN: @PostConstruct fires during bean creation.
// Other beans that depend on tenant config may not exist yet.
@Component
public class TenantConfigLoader {

    private final TenantRepository tenantRepository;
    private final CacheManager cacheManager;  // May not be fully initialized

    @PostConstruct
    public void loadTenants() {
        // CacheManager's @PostConstruct may not have fired yet
        // if this bean is instantiated first
        List<Tenant> tenants = tenantRepository.findAll();
        Cache cache = cacheManager.getCache("tenants");  // NPE: cache not initialized
        tenants.forEach(t -> cache.put(t.getId(), t));
    }
}

@PostConstruct fires during finishBeanFactoryInitialization, as each bean is created. The order of bean creation within that phase depends on dependency resolution order. If TenantConfigLoader is created before CacheManager completes its own @PostConstruct, the cache is not ready.

The Correct Pattern

// CORRECT: Use SmartInitializingSingleton to guarantee
// all singletons (including CacheManager) are fully initialized.
@Component
public class TenantConfigLoader implements SmartInitializingSingleton {

    private final TenantRepository tenantRepository;
    private final CacheManager cacheManager;

    public TenantConfigLoader(TenantRepository tenantRepository,
                               CacheManager cacheManager) {
        this.tenantRepository = tenantRepository;
        this.cacheManager = cacheManager;
    }

    @Override
    public void afterSingletonsInstantiated() {
        List<Tenant> tenants = tenantRepository.findAll();
        Cache cache = cacheManager.getCache("tenants");
        tenants.forEach(t -> cache.put(t.getId(), t));
    }
}

SmartInitializingSingleton.afterSingletonsInstantiated() fires after every singleton bean has been created and fully initialized, including their @PostConstruct methods. This is the correct hook for cross-bean initialization that depends on the entire singleton graph being ready.

If the initialization must happen after the web server is ready to accept traffic (for example, registering the service with a discovery server), use ApplicationRunner or listen for ApplicationReadyEvent. These fire after refresh() returns and the embedded server is bound to its port.

Pick the narrowest hook that satisfies the timing requirement. @PostConstruct for self-contained bean initialization. SmartInitializingSingleton for cross-bean initialization. ApplicationRunner for post-startup tasks. ApplicationReadyEvent for tasks that should only run when the application is fully ready to serve.