Skip to main content
spring internals

ApplicationContext Lifecycle: From SpringApplication.run() to a Fully Wired Application, Step by Step

6 min read Chapter 1 of 78

ApplicationContext Lifecycle

SpringApplication.run(MyApp.class, args) is where every Spring Boot application begins. One static method call. What happens between that call and the moment your first HTTP request is served involves over forty discrete steps, a dozen extension interfaces, three separate phases of bean processing, and a strict ordering that, when violated, produces startup failures that no amount of annotation shuffling will fix.

ApplicationContext refresh() lifecycle showing the twelve phases from SpringApplication.run() through context ready

This book is about a multi-tenant SaaS backend. REST and reactive API layers, JWT-secured endpoints, transactional order processing, async notification dispatch, Spring Data JPA repositories, a custom starter shared across four microservices, and a Spring Cloud service mesh connecting them. Every chapter uses this system. Every code example runs inside it. Every failure scenario happens because someone on the team understood the annotation but not the mechanism beneath it.

Three positions run through every chapter. State them now.

The annotation is never the explanation. @Transactional, @Autowired, @EnableAutoConfiguration, @PreAuthorize all hide infrastructure that determines whether your application works correctly under load, fails silently in production, or takes thirty minutes to start in a test suite. The annotation is where the chapter starts. The mechanism is where it ends.

Proxies are the answer to almost every Spring mystery. Self-invocation breaking @Transactional. @Cacheable not firing on internal calls. @Async doing nothing. Security annotations bypassed on direct method calls. Every one of these is a proxy problem. The proxy chapters are the most important chapters in this book.

Auto-configuration is an engineering discipline, not magic. The condition evaluation chain is deterministic and inspectable. Engineers who understand it debug bean registration failures in minutes. Engineers who treat it as magic spend hours.

The Phase Sequence

SpringApplication.run() executes in this order:

  1. Create the SpringApplication instance and detect the application type (servlet, reactive, or none)
  2. Load SpringApplicationRunListener instances from spring.factories / META-INF/spring/
  3. Fire ApplicationStartingEvent
  4. Prepare the Environment: load property sources, resolve profiles, bind external configuration
  5. Fire ApplicationEnvironmentPreparedEvent
  6. Print the banner
  7. Create the ApplicationContext (the specific type depends on step 1: AnnotationConfigServletWebServerApplicationContext for servlet, AnnotationConfigReactiveWebServerApplicationContext for reactive)
  8. Prepare the context: set the environment, register BeanFactoryPostProcessor instances, load bean definitions
  9. Refresh the context (this is where the heavy work happens)
  10. Fire ApplicationStartedEvent
  11. Call all ApplicationRunner and CommandLineRunner beans
  12. Fire ApplicationReadyEvent

Step 9 deserves its own section. The refresh() method on AbstractApplicationContext is the most important method in the entire Spring Framework. It orchestrates bean definition registration, bean factory post-processing, bean post-processor registration, singleton pre-instantiation, and event multicaster initialization, in that exact order.

Inside refresh()

The AbstractApplicationContext.refresh() method calls thirteen internal methods in sequence. The order is not arbitrary. Each step depends on the previous one having completed.

// CORRECT: The actual refresh sequence (simplified from AbstractApplicationContext)
public void refresh() {
    prepareRefresh();                      // Set startup date, validate required properties
    ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    prepareBeanFactory(beanFactory);        // Register default beans: environment, systemProperties
    postProcessBeanFactory(beanFactory);    // Template method for subclass customization
    invokeBeanFactoryPostProcessors(beanFactory);  // Run BeanFactoryPostProcessors (bean definitions modified here)
    registerBeanPostProcessors(beanFactory);       // Register BeanPostProcessors (but don't invoke yet)
    initMessageSource();                   // i18n support
    initApplicationEventMulticaster();     // Event broadcasting
    onRefresh();                           // Template method: embedded server starts here
    registerListeners();                   // Register ApplicationListener beans
    finishBeanFactoryInitialization(beanFactory);  // Instantiate all non-lazy singletons
    finishRefresh();                       // Publish ContextRefreshedEvent
}

The critical ordering constraint: invokeBeanFactoryPostProcessors runs before registerBeanPostProcessors, which runs before finishBeanFactoryInitialization. This means bean definitions can be modified before post-processors are registered, and post-processors are registered before beans are created. Violate this ordering by registering a bean that depends on a bean factory post-processor result, and you get a BeanCurrentlyInCreationException with a stack trace that tells you nothing about the actual problem.

Making the Lifecycle Visible

The lifecycle is not theoretical. You can observe every phase in a running application.

// CORRECT: Logging listener that prints every lifecycle phase
@Component
public class LifecycleTracer implements ApplicationListener<ApplicationEvent> {

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

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        log.info("Lifecycle event: {} at {}",
            event.getClass().getSimpleName(),
            System.currentTimeMillis());
    }
}

Run the SaaS backend with --debug and Spring Boot prints the full condition evaluation report, including which auto-configurations were applied, which were skipped, and why. This report is the single most useful debugging tool for startup issues.

java -jar saas-backend.jar --debug

Output includes:

============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource',
        'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'

Negative matches:
-----------------
   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
           'jakarta.jms.ConnectionFactory'

The Failure Mode

The most common lifecycle failure in the SaaS backend: a @Configuration class that depends on a bean created by auto-configuration, but is processed before auto-configuration runs.

// BROKEN: This @Configuration class depends on DataSource,
// but if it's scanned before DataSourceAutoConfiguration runs,
// DataSource does not exist yet.
@Configuration
public class TenantDataSourceConfig {

    @Bean
    public TenantRoutingDataSource tenantDataSource(DataSource defaultDataSource) {
        TenantRoutingDataSource routing = new TenantRoutingDataSource();
        routing.setDefaultTargetDataSource(defaultDataSource);
        return routing;
    }
}

The symptom: NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available. The engineer adds @DependsOn("dataSource"), which sometimes works and sometimes does not, depending on the order in which configuration classes are processed.

The Correct Pattern

// CORRECT: Use @AutoConfiguration with explicit ordering,
// or declare the dependency in a way that respects the lifecycle.
@Configuration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class TenantDataSourceConfig {

    @Bean
    @ConditionalOnBean(DataSource.class)
    public TenantRoutingDataSource tenantDataSource(DataSource defaultDataSource) {
        TenantRoutingDataSource routing = new TenantRoutingDataSource();
        routing.setDefaultTargetDataSource(defaultDataSource);
        return routing;
    }
}

The @AutoConfigureAfter annotation tells the auto-configuration machinery to process this class after DataSourceAutoConfiguration. The @ConditionalOnBean guard prevents the bean from being registered if DataSource is absent for any reason. Together, they encode the lifecycle dependency explicitly rather than relying on classpath scanning order, which is not guaranteed and changes between Spring Boot versions.

The lifecycle is not a suggestion. It is an execution contract. Every extension point in this book, from BeanFactoryPostProcessor to ApplicationListener to auto-configuration ordering, exists because the lifecycle demands that certain work happens at certain phases. Understanding the phase sequence is the prerequisite for everything that follows.