How Spring Boot Starts: SpringApplication.run() Step by Step
Every Spring Boot application starts with one line: SpringApplication.run(). Behind it are about ten well-defined phases, and each phase publishes an event that you can hook into. If you understand this workflow, you can debug slow startups, write EnvironmentPostProcessors, custom listeners and failure analyzers with confidence.
The big picture
SpringApplication.run(App.class, args)
│
├─ [A] new SpringApplication(App.class)
│ ├─ deduce application type: SERVLET / REACTIVE / NONE (look at the classpath)
│ ├─ load ApplicationContextInitializer + ApplicationListener (META-INF/spring.factories)
│ └─ deduce the main class
│
└─ [B] run(args)
1. StopWatch starts, BootstrapContext created
2. RunListeners ─► event: ApplicationStartingEvent
3. prepareEnvironment
├─ create Environment (system props, env vars, command line args)
├─ event: ApplicationEnvironmentPreparedEvent
│ └─ EnvironmentPostProcessors run: application.yml, profiles, spring.config.import
└─ bind spring.main.* properties to the SpringApplication
4. print the banner
5. createApplicationContext() (AnnotationConfigServletWebServerApplicationContext)
6. prepareContext
├─ set Environment, run ApplicationContextInitializers
├─ event: ApplicationContextInitializedEvent
├─ register the main class as a bean definition
└─ event: ApplicationPreparedEvent
7. refreshContext ──► context.refresh() (see the next page for the 12 steps)
├─ scan components, run auto-configuration, create all singleton beans
└─ create and START the embedded Tomcat
8. event: ApplicationStartedEvent
9. callRunners: ApplicationRunner and CommandLineRunner beans (ordered)
10. event: ApplicationReadyEvent ◄── the application is now ready for traffic
(on any exception) event: ApplicationFailedEvent + FailureAnalyzers print a friendly messageEvents in order and what you can do in each
| Event | When | Typical use |
|---|---|---|
| ApplicationStartingEvent | Very early, before Environment and context exist | Register early logging or system setup. Only listeners registered in spring.factories or via SpringApplication see it |
| ApplicationEnvironmentPreparedEvent | Environment ready, context not created | Add or change property sources (EnvironmentPostProcessor) |
| ApplicationContextInitializedEvent | Context created, no bean definitions loaded | Rarely used |
| ApplicationPreparedEvent | Bean definitions loaded, beans not created | Change bean definitions programmatically |
| ContextRefreshedEvent | Context refreshed (this is a Spring event) | Runs also for child contexts, be careful |
| WebServerInitializedEvent | Web server started | Read the real port when server.port=0 |
| ApplicationStartedEvent | Context refreshed, runners not called yet | Warm-up tasks that should run before runners |
| AvailabilityChangeEvent (LIVENESS) | Application is alive | Health probes |
| ApplicationReadyEvent | Runners finished, app is ready | Start background consumers, log 'ready', prime caches |
| AvailabilityChangeEvent (READINESS) | Ready to accept traffic | Kubernetes readiness |
| ApplicationFailedEvent | Startup failed | Send an alert, clean up |
Hooking into the phases: examples
// 1. Listener for ApplicationReadyEvent (works as a normal bean)
@Component
public class ReadyLogger {
private static final Logger log = LoggerFactory.getLogger(ReadyLogger.class);
@EventListener(ApplicationReadyEvent.class)
public void onReady(ApplicationReadyEvent event) {
log.info("Application ready in {}", event.getTimeTaken());
}
@EventListener
public void onWebServer(WebServerInitializedEvent event) {
log.info("Web server is listening on port {}", event.getWebServer().getPort());
}
}
// 2. Runners with an order
@Component @Order(1)
class FirstRunner implements ApplicationRunner {
public void run(ApplicationArguments args) { System.out.println("runner 1, option names=" + args.getOptionNames()); }
}
@Component @Order(2)
class SecondRunner implements CommandLineRunner {
public void run(String... args) { System.out.println("runner 2, raw args=" + Arrays.toString(args)); }
}EnvironmentPostProcessor: add configuration before beans exist
This runs in the phase after the Environment is prepared. Use it to load secrets, decrypt values or compute properties. It must be registered in META-INF/spring/org.springframework.boot.EnvironmentPostProcessor.imports (Boot 3.x older versions used spring.factories).
public class SecretsEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication application) {
Map<String, Object> secrets = new HashMap<>();
secrets.put("app.api-key", System.getenv().getOrDefault("MY_API_KEY", "dev-key"));
// added with the HIGHEST priority
env.getPropertySources().addFirst(new MapPropertySource("secrets", secrets));
}
}
// file: META-INF/spring/org.springframework.boot.EnvironmentPostProcessor.imports
// com.example.SecretsEnvironmentPostProcessorCustomize SpringApplication
public static void main(String[] args) {
SpringApplication app = new SpringApplication(DemoApplication.class);
app.setBannerMode(Banner.Mode.OFF);
app.setAdditionalProfiles("metrics");
app.setLazyInitialization(false);
app.addListeners(new MyStartingListener());
app.run(args);
}
// Fluent style:
// new SpringApplicationBuilder(DemoApplication.class).web(WebApplicationType.NONE).run(args);Find out why startup is slow
SpringApplication app = new SpringApplication(DemoApplication.class);
app.setApplicationStartup(new BufferingApplicationStartup(4096)); // records every startup step
app.run(args);
// then: management.endpoints.web.exposure.include=startup
// curl -X POST localhost:8080/actuator/startup (JSON timeline of bean creation)- Typical slow parts: many auto-configurations, slow bean constructors, Hibernate schema validation, connecting to remote systems in @PostConstruct.
- Try spring.main.lazy-initialization=true to see if bean creation is the cause. It moves cost to first use, so it is a diagnostic tool, not a fix.
- Measure with the startup endpoint, then fix the slowest beans. Native image and CDS/AOT cache improve startup at the platform level.
Friendly failure messages: FailureAnalyzer
public class MissingApiKeyFailureAnalyzer extends AbstractFailureAnalyzer<MissingApiKeyException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, MissingApiKeyException cause) {
return new FailureAnalysis("The property app.api-key is missing.",
"Set the environment variable MY_API_KEY or the property app.api-key.", cause);
}
}
// Register in META-INF/spring.factories:
// org.springframework.boot.diagnostics.FailureAnalyzer=com.example.MissingApiKeyFailureAnalyzerExample of the built-in analyzers: the well known message 'APPLICATION FAILED TO START, Description: ... Action: ...' for Port 8080 was already in use is produced by a FailureAnalyzer.
Common mistake: Doing heavy work in a constructor or @PostConstruct of many beans (for example calling remote services). All startup work runs on the main thread, one bean after another, and delays readiness. Move slow warm-up work to an ApplicationReadyEvent listener or an async task.
Interview questions
What is the difference between ApplicationStartedEvent and ApplicationReadyEvent?
Started is published after the context is refreshed but before the runners run. Ready is published after all ApplicationRunner and CommandLineRunner beans finished, so it is the real signal that the application is ready.
Where do I put code that must run before any bean is created?
In an EnvironmentPostProcessor, an ApplicationContextInitializer or a BeanFactoryPostProcessor, depending on what you need to change.
How does Spring Boot decide it is a web application?
By the classpath: if Servlet API and Spring MVC are present it is SERVLET, if Spring WebFlux is present and MVC is not it is REACTIVE, otherwise NONE. You can override it with spring.main.web-application-type.