What Actually Happens When You Launch a Spring Boot App

You add a dependency, write a main method, hit Run — and a fully configured web server appears. No XML. No wiring ceremony. It just works.
That experience is great. Right up until something breaks. At that point, calling it “magic” leaves you with no map and no starting point.
Let’s build that map.
⚡ TL;DR (Quick Recap)
- @SpringBootApplication = @SpringBootConfiguration + @ComponentScan + @EnableAutoConfiguration
- Auto-configuration is conditional — your beans always override Spring Boot’s defaults
- Spring Boot 3.x reads auto-config candidates from META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
- Spring Boot 4.x full modularization of the spring-boot-autoconfigure artifact into domain-specific modules
- debug=true in application.properties prints every startup decision Spring Boot made
Why You Need to Know This
Spring Boot 3.x
- Java 17+
- javax.* to jakarta.* namespace migration
Spring Boot 4.x
- Java 17+, Java 21 (LTS) and Java 25+ as the recommended targets to take advantage of virtual threads and modern GC
- Jakarta EE 11 (Servlet 6.1, JPA 3.2)
- Monolithic autoconfigure split into domain modules
The bigger point is this: most BeanCreationException errors, circular dependencies and misconfigured datasources happen during startup, in a predictable sequence. Knowing that sequence lets you skip the trial-and-error restarts.
It All Starts With main()
Every Spring Boot application starts as a standard Java program:
@SpringBootApplication
public class BootNotesApp {
public static void main(String[] args) {
SpringApplication.run(BootNotesApp.class, args);
}
}
SpringApplication.run() immediately detects your application type — servlet-based (Tomcat), reactive (Netty) or non-web — and picks the appropriate ApplicationContext implementation.
It also loads ApplicationListener and ApplicationContextInitializer implementations from the classpath. These are the hooks that third-party libraries use to plug into startup.
@SpringBootApplication Is Three Annotations in One
This single annotation is doing more than it looks:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // marks this class as a @Configuration source
@ComponentScan // scans sub-packages for @Component, @Service, @Repository
@EnableAutoConfiguration // activates Spring Boot's auto-configuration engine
public @interface SpringBootApplication { ... }
- @ComponentScan finds your code — controllers, services, repositories in the same package and below.
- @EnableAutoConfiguration finds everything Spring Boot adds on your behalf.
Both are necessary. Remove either one and the experience breaks in a very different direction.
If a developer accidentally places their main application class in src/main/java without a defined package structure, Spring Boot attempts to scan the entire classpath, causing massive startup slowdowns or bean conflicts.
Auto-Configuration Is Just Conditional Beans
This is the most important concept to internalize.
Auto-configuration is not a special runtime mechanism. It is a collection of regular @Configuration classes, each protected by @Conditional annotations. Spring Boot ships with hundreds of them.
In Spring Boot 3.x, the list of candidates is read from:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
The legacy spring.factories file is no longer used for auto-configuration. Each entry in that file is evaluated against conditions before a single bean is created.
Spring Boot 4’s massive internal architectural shift: Massive Modularization. The historically monolithic spring-boot-autoconfigure module was broken down into tiny, focused domain modules.
Here is a simplified version of what DataSourceAutoConfiguration does:
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
public class DataSourceAutoConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
}
The conditions tell the full story:
- @ConditionalOnClass — only activates if a specific class is present on the classpath
- @ConditionalOnMissingBean — backs off immediately if you've already defined that bean yourself
- @ConditionalOnProperty — activates only if a property is set to a specific value
The rule is simple: define your own bean and Spring Boot’s default disappears.
The Bean Lifecycle Plays Out
Once configuration classes are processed, the ApplicationContext refreshes. This is where beans come to life, in a fixed sequence:
- Bean definitions are registered.
- Beans are instantiated via constructors.
- Fields and setter dependencies are injected.
- BeanPostProcessor pre-initialization runs.
- @PostConstruct initializers execute.
- BeanPostProcessor post-initialization fires (Proxies & AOP wrappers are created here).
- ApplicationRunner / CommandLineRunner beans fire - post-startup tasks execute last
A BeanCreationException during context refresh almost always traces to a missing dependency, a circular reference, or a failed condition — knowing the sequence tells you which step to look at.
Diagnostic Tools: Auditing Startup Decisions
The fastest debugging tool Spring Boot ships is already in your project:
# application.properties
debug=true
When you restart, your console will output a massive Conditions Evaluation Report. It will show you exactly which auto-configurations matched (and why) and which ones were skipped. Every auto-configuration candidate. Every decision. Every reason. This is more useful than most debugging sessions.
CONDITIONS EVALUATION REPORT
=============================
ConditionEvaluationReport being printed to ...
JpaAuditingAutoConfiguration:
Did not match:
- @ConditionalOnBean (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans of type javax.sql.DataSource (OnBeanCondition)
Matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.data.jpa.repository.JpaRepository'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- @ConditionalOnProperty (spring.data.jpa.repositories.enabled=true) matched (OnPropertyCondition)
The /actuator/conditions endpoint is a built-in Spring Boot Actuator feature that provides a report on the evaluation of conditions for configuration and auto-configuration classes (depends on spring-boot-starter-actuator). It is primarily used for debugging, why specific Spring features did or did not load in an application.
# application.properties
management.endpoints.web.exposure.include=conditions
# Then hit:
# GET http://localhost:8080/actuator/conditions
Executing Code Post-Startup
@Component
public class AppStartupRunner implements ApplicationRunner {
private static final Logger logger = LoggerFactory.getLogger(AppStartupRunner.class);
@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("Application started successfully with arguments: {}", Arrays.toString(args.getSourceArgs()));
}
}
Final Takeaways
Spring Boot 4’s startup is a clean, deterministic chain: detect the app type, scan your code, evaluate auto-configuration conditions, wire the bean lifecycle, start the server.
Three things to carry forward:
- @SpringBootApplication is a shortcut with three distinct jobs — know what each one does
- Your beans always win — auto-configuration is built to step aside the moment you define your own
- debug=true is your startup x-ray — run it on any project you're debugging and read the report
The next time your app boots in two seconds, you’ll know exactly what happened in those two seconds. That knowledge is the difference between using a framework and understanding one.
Originally posted on marconak-matej.medium.com.