What Actually Happens When Spring Boot Starts Up
by Eric Hanson, Backend Developer at Clean Systems Consulting
The startup sequence
Spring Boot startup is not a single operation. It's a sequence of phases, each with specific responsibilities:
- SpringApplication initialization — determines the application type (servlet, reactive, or none), loads
ApplicationContextInitializerandApplicationListenerimplementations fromspring.factories/AutoConfiguration.imports - Environment preparation — loads
application.properties/application.yml, resolves profiles, applies property sources in precedence order - ApplicationContext creation — creates the
AnnotationConfigServletWebServerApplicationContext(for web applications) or appropriate subtype - Bean definition loading — component scanning,
@Configurationclass processing,@Beanmethod registration - Auto-configuration — Spring Boot's auto-configuration classes are evaluated and conditionally applied
- Context refresh — all bean definitions are instantiated, dependencies injected, lifecycle callbacks invoked
- Web server start — embedded Tomcat/Jetty/Undertow starts and begins accepting connections
- ApplicationReadyEvent — the application is ready to serve requests
Understanding which phase each piece of code runs in explains startup ordering bugs and why some configurations must appear before others.
Phase 2: Property loading order matters
Spring Boot loads properties from multiple sources in a specific precedence order (highest to lowest):
- Command-line arguments (
--server.port=9090) SPRING_APPLICATION_JSONenvironment variable- OS environment variables
application-{profile}.properties(profile-specific, outside jar)application.properties(outside jar)application-{profile}.properties(inside jar)application.properties(inside jar)@PropertySourceannotations on@Configurationclasses- Default properties
The practical implication: environment variables override properties files, which override @PropertySource. Container orchestration that sets environment variables will always win over bundled configuration — intentionally. @PropertySource annotations are loaded late (phase 4, during @Configuration processing) — they cannot override application.properties entries.
Profile activation order. Multiple profiles are processed in reverse order — the last profile listed wins for overlapping properties:
# application.yml
spring:
profiles:
active: db-prod, logging-verbose
# logging-verbose's properties override db-prod's for any overlap
Phase 4-5: Auto-configuration and @Conditional
Auto-configuration is Spring Boot's mechanism for applying sensible defaults based on what's on the classpath and in the environment. Each auto-configuration class is annotated with @Conditional annotations that determine whether it applies:
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // applies only if DataSource is on classpath
@ConditionalOnMissingBean(DataSource.class) // applies only if no DataSource bean exists
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
@ConditionalOnMissingBean is the key pattern: auto-configuration applies only if the application hasn't already defined the bean. This is how you override auto-configured beans — define your own @Bean of the same type, and the auto-configuration backs off.
The auto-configuration classes to apply are listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Spring Boot 2.7+, replacing spring.factories). Spring Boot reads this file from every JAR on the classpath during startup. Libraries that provide auto-configuration ship this file — this is how Spring Data JPA, Spring Security, and Actuator wire themselves automatically.
Debugging auto-configuration. When a bean isn't being created as expected, the auto-configuration report shows which configurations applied and which didn't — and why:
--debug flag or logging.level.org.springframework.boot.autoconfigure=DEBUG
This produces a "CONDITIONS EVALUATION REPORT" in the startup log showing each auto-configuration class, whether it matched, and the condition that failed if not.
Phase 6: Context refresh and bean instantiation order
During context refresh, Spring instantiates all singleton beans eagerly by default. The instantiation order follows the dependency graph — a bean is not instantiated until all its dependencies are ready.
@DependsOn for non-obvious dependencies. When bean A must be initialized before bean B, but A is not a constructor dependency of B — a common case with database migration tools like Flyway or Liquibase:
@Bean
@DependsOn("flywayInitializer") // ensure Flyway runs before JPA initializes schema
public LocalContainerEntityManagerFactoryBean entityManagerFactory(...) {
...
}
Spring Boot handles the Flyway/JPA ordering automatically via FlywayAutoConfiguration. For custom tools or uncommon ordering requirements, @DependsOn makes the dependency explicit.
@Lazy for deferred initialization. By default, singleton beans are instantiated at startup. @Lazy defers instantiation to first use:
@Service
@Lazy
public class ExpensiveInitializationService {
public ExpensiveInitializationService() {
// called only when first injected, not at startup
}
}
spring.main.lazy-initialization=true makes all beans lazy globally — faster startup, but startup-time errors are deferred to first use. Useful for development; evaluate carefully for production where fast-fail-at-startup is a feature.
Circular dependencies. A circular dependency — bean A depends on bean B, bean B depends on bean A — causes a BeanCurrentlyInCreationException with constructor injection (the default and preferred form). Spring can resolve circular dependencies with field or setter injection by using a partially constructed proxy, but this is a design smell. The correct fix is to break the cycle by extracting a third class that both depend on, or by restructuring the dependency direction.
Lifecycle callbacks — the order that matters
After a bean is instantiated and its dependencies injected, lifecycle callbacks fire in this order:
BeanPostProcessor.postProcessBeforeInitialization()— framework hooks, runs before user code@PostConstructmethod — user initialization codeInitializingBean.afterPropertiesSet()— older interface-based initialization@Bean(initMethod = "init")— XML-style initialization methodBeanPostProcessor.postProcessAfterInitialization()— framework hooks, AOP proxies created here
@Service
public class CacheWarmingService {
@PostConstruct
public void warmCache() {
// Runs after all dependencies are injected, before the application is ready
// Safe to call other beans here — they're all initialized
loadFrequentlyAccessedData();
}
}
@PostConstruct is the correct place for initialization that requires injected dependencies. The constructor is too early — dependencies may not be injected via field injection. ApplicationReadyEvent (phase 8) is for initialization that should run after the web server starts.
@PreDestroy is the inverse — called during graceful shutdown before the bean is destroyed. Use it to release resources, close connections, flush queues:
@PreDestroy
public void cleanup() {
scheduler.shutdown();
connectionPool.close();
}
Phase 7-8: Web server startup and readiness
The embedded web server starts after all beans are initialized. At this point, the application can accept HTTP requests. Spring Boot fires events at each transition:
@Component
public class StartupListener {
@EventListener(ApplicationReadyEvent.class)
public void onReady(ApplicationReadyEvent event) {
// Runs after web server is started and application is ready for requests
// Use for: registering with service discovery, warming HTTP caches,
// starting background tasks that need the full application context
log.info("Application started in {}ms",
event.getTimeTaken().toMillis());
}
@EventListener(ApplicationStartedEvent.class)
public void onStarted() {
// Runs after context refresh but before web server starts
// Use for: pre-start validation that should block startup on failure
}
}
ApplicationReadyEvent vs ApplicationStartedEvent: ApplicationStartedEvent fires after the context refresh but before the web server accepts connections. ApplicationReadyEvent fires after the web server is started and the application is accepting requests. For tasks that should run before serving traffic, ApplicationStartedEvent. For tasks that only make sense once the application is live, ApplicationReadyEvent.
Startup time profiling
Spring Boot 2.5+ includes startup actuator data:
management.endpoints.web.exposure.include=startup
GET /actuator/startup returns a timeline of startup events with durations. Identify the slow phases — long bean initialization, slow auto-configuration, expensive @PostConstruct — and optimize them.
The --debug flag produces the full conditions evaluation report and bean definition loading log. For large applications where startup time matters, spring-context-indexer pre-computes the component scan index at build time, eliminating classpath scanning at startup:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<optional>true</optional>
</dependency>
This generates a META-INF/spring.components file at build time. At runtime, Spring reads the index instead of scanning the classpath. For large applications, this can cut startup time by 30–50%.
The startup bug categories that the sequence explains
Bean not found despite being annotated. Component scanning didn't reach the package. Either the @SpringBootApplication base package doesn't cover the bean's package, or the bean's package is missing from @ComponentScan. Check the component scan coverage.
Auto-configuration not backing off. A custom @Bean didn't suppress auto-configuration. The auto-configuration's @ConditionalOnMissingBean looks for an exact type match — if your bean is a subtype or implements the interface but isn't the exact type, the condition doesn't fire. Check the conditions evaluation report.
Property not resolved. A property reference like ${my.prop} results in a PlaceholderResolutionException. Either the property isn't in any loaded source, or the source is loaded at the wrong phase (a @PropertySource loaded in phase 4 can't override environment variables loaded in phase 2).
@PostConstruct fails. A dependency isn't ready. If the @PostConstruct method calls an external service — database, HTTP endpoint — and that service isn't available at startup, the failure prevents the bean from initializing and cascades through the bean graph. Move external service calls to ApplicationReadyEvent if startup should not be blocked by external unavailability.
Each of these bugs has a specific cause rooted in startup phase ordering. Knowing the sequence makes the diagnosis systematic rather than guesswork.