A deep-dive into how Spring Boot reduces configuration and starts your app automatically. Great for interview prep and understanding the framework under the hood.
- What Happens When You Run a Spring Boot App
- @SpringBootApplication Explained
- Auto-Configuration Internals
- Conditional Configuration
- Starter Dependencies
- Embedded Server Internals
- SpringApplication Internals
- Bean Lifecycle in Boot
- External Configuration Binding
- Actuator Internals
- How Boot Reduces Boilerplate
- Disabling Auto-Configuration
- Important Internal Classes
- Full Execution Flow
- Custom Auto-Configuration
- Spring Boot Profiles
- Property Source Priority Order
- Failsafe Interview Answer
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}Behind the scenes, in order:
SpringApplication.run()starts- Creates
ApplicationContext - Performs auto-configuration
- Scans components
- Starts embedded server (Tomcat / Jetty / Undertow)
- Application is ready
@SpringBootApplication is a meta-annotation that combines three annotations:
@SpringBootConfiguration // Same as @Configuration — marks class as config source
@EnableAutoConfiguration // Triggers Spring Boot magic (most important)
@ComponentScan // Scans @Component, @Service, @Repository, @Controller, @RestController| Annotation | Purpose |
|---|---|
@Component |
Generic Spring-managed bean |
@Service |
Business logic layer |
@Repository |
Data access layer (also wraps persistence exceptions) |
@Controller |
MVC controller |
@RestController |
@Controller + @ResponseBody |
This is the core of Spring Boot and where most interview questions originate.
When @EnableAutoConfiguration runs, it triggers AutoConfigurationImportSelector, which reads:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Note: In Spring Boot 2.x this was
META-INF/spring.factories. Boot 3.x migrated to the.importsfile.
That file contains a list of configuration classes, for example:
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
Spring loads them conditionally — not all at once.
Auto-config works because of @Conditional annotations. These are the "magic" conditions:
| Annotation | Meaning |
|---|---|
@ConditionalOnClass |
Activate if class is on classpath |
@ConditionalOnMissingBean |
Activate if bean is NOT already defined |
@ConditionalOnProperty |
Activate if a property is set to a value |
@ConditionalOnMissingClass |
Activate if class is NOT on classpath |
@ConditionalOnBean |
Activate if a specific bean IS already defined |
@ConditionalOnWebApplication |
Activate only in a web context |
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
public class DataSourceAutoConfiguration {
// Creates a default DataSource bean
}Translation:
- If
DataSourceclass exists on the classpath AND - No
DataSourcebean is manually defined - THEN create a default
DataSourcebean
Boot says: "If you don't configure it, I'll configure it for you."
But if you define your own bean → Boot backs off automatically.
Starters are curated dependency bundles. They put the right classes on the classpath so auto-configuration can trigger.
Pulls in:
spring-webspring-webmvcjackson(JSON serialization)tomcat(embedded server)validation
Because the required classes are now on the classpath → WebMvcAutoConfiguration, DispatcherServletAutoConfiguration, etc. all trigger.
Key insight: Classpath presence = Feature activation
| Starter | What it activates |
|---|---|
spring-boot-starter-data-jpa |
Hibernate, JPA, DataSource |
spring-boot-starter-security |
Spring Security filter chain |
spring-boot-starter-test |
JUnit 5, Mockito, AssertJ |
spring-boot-starter-actuator |
Health, metrics, info endpoints |
spring-boot-starter-webflux |
Reactive Netty server |
Spring Boot embeds the server using ServletWebServerApplicationContext.
| Dependency | Server |
|---|---|
spring-boot-starter-web |
Tomcat (default) |
spring-boot-starter-web + Jetty excluded |
Jetty |
spring-boot-starter-webflux |
Netty (reactive) |
Boot creates a TomcatServletWebServerFactory and starts Tomcat programmatically — no web.xml needed.
<!-- Exclude Tomcat and add Jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>SpringApplication.run() goes through five steps:
Detects the application type:
| Detection | Type |
|---|---|
DispatcherServlet on classpath |
Servlet-based web app |
DispatcherHandler on classpath |
Reactive web app |
| Neither | Non-web / CLI app |
Loads properties from (in order):
application.properties/application.yml- Active profiles (
application-{profile}.properties) - Command-line args
- System environment variables
| App Type | Context Class |
|---|---|
| Servlet | AnnotationConfigServletWebServerApplicationContext |
| Reactive | AnnotationConfigReactiveWebServerApplicationContext |
| Non-web | AnnotationConfigApplicationContext |
ApplicationContextInitializer— runs before context refreshApplicationListener— reacts to application events (e.g.,ApplicationReadyEvent)
This is where everything happens:
- Beans are created
- Dependencies injected
- Auto-configuration executed
- Embedded server started
During context refresh, beans go through this lifecycle:
Bean definitions loaded
↓
BeanFactoryPostProcessor runs ← modifies bean definitions (e.g., PropertySourcesPlaceholderConfigurer)
↓
BeanPostProcessor registered ← will wrap/proxy beans later
↓
Beans instantiated (constructor called)
↓
Dependency injection (@Autowired, @Value)
↓
BeanPostProcessor#postProcessBeforeInitialization
↓
@PostConstruct / afterPropertiesSet()
↓
BeanPostProcessor#postProcessAfterInitialization ← AOP proxies created here
↓
Bean is ready
↓
@PreDestroy / destroy() ← on context shutdown
Spring Boot binds external properties to beans using @ConfigurationProperties and the Binder API.
@ConfigurationProperties(prefix = "server")
public class ServerProperties {
private int port;
// getters and setters
}server.port=8081Spring Boot supports multiple formats for the same property:
| Format | Example |
|---|---|
| Kebab-case (canonical) | server.port |
| Camel-case | server.Port |
| Underscore | server_port |
| UPPERCASE env var | SERVER_PORT |
Adding spring-boot-starter-actuator triggers auto-configuration of production-ready endpoints:
| Endpoint | Path | Description |
|---|---|---|
health |
/actuator/health |
App health status |
metrics |
/actuator/metrics |
Micrometer metrics |
beans |
/actuator/beans |
All registered beans |
env |
/actuator/env |
Environment properties |
conditions |
/actuator/conditions |
Auto-config conditions report |
mappings |
/actuator/mappings |
All @RequestMapping paths |
All endpoints are conditionally loaded via @ConditionalOnAvailableEndpoint.
| Without Spring Boot | With Spring Boot |
|---|---|
XML configs (applicationContext.xml) |
Zero XML required |
Manual DispatcherServlet registration |
Auto-configured |
Manual DataSource creation |
Auto-configured from properties |
Manual server setup (web.xml, WAR) |
Embedded server, runnable JAR |
| Manual transaction manager | Auto-configured |
Dozens of @Bean methods |
Sensible defaults via auto-config |
Option 1 — Annotation-level:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class App { ... }Option 2 — Properties:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationOption 3 — Exclude specific auto-config at test level:
@SpringBootTest
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
class MyTest { ... }| Class | Role |
|---|---|
SpringApplication |
Entry point, bootstraps the application |
AutoConfigurationImportSelector |
Reads and imports auto-config classes |
ConditionEvaluator |
Evaluates @Conditional annotations |
SpringFactoriesLoader |
Loads classes from META-INF/spring.factories (Boot 2) |
Binder |
Binds external properties to @ConfigurationProperties |
ConfigurationClassPostProcessor |
Processes @Configuration classes |
TomcatServletWebServerFactory |
Creates and starts the embedded Tomcat instance |
BeanDefinitionLoader |
Loads bean definitions from sources |
ConditionEvaluationReport |
Records which conditions passed/failed (visible via /actuator/conditions) |
main()
↓
SpringApplication.run()
↓
Detect application type (Servlet / Reactive / None)
↓
Load SpringApplicationRunListeners
↓
Prepare Environment (properties, profiles, args)
↓
Print Banner
↓
Create ApplicationContext
↓
Load AutoConfiguration classes (from .imports file)
↓
Apply @Conditional checks
↓
Register & create Beans
↓
BeanPostProcessor processing (AOP, validation, etc.)
↓
Start Embedded Server
↓
Publish ApplicationReadyEvent
↓
Application is ready 🚀
You can write your own auto-configuration — useful when building libraries or internal frameworks.
1. Create a configuration class:
@Configuration
@ConditionalOnClass(MyService.class)
@ConditionalOnMissingBean(MyService.class)
public class MyServiceAutoConfiguration {
@Bean
public MyService myService() {
return new MyService();
}
}2. Register it (Spring Boot 3.x):
Create src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.example.MyServiceAutoConfiguration
For Spring Boot 2.x, register in META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyServiceAutoConfigurationProfiles let you switch configuration between environments (dev, staging, prod).
# application.properties
spring.profiles.active=dev# application-dev.properties
server.port=8080
spring.datasource.url=jdbc:h2:mem:devdb
# application-prod.properties
server.port=80
spring.datasource.url=jdbc:postgresql://prod-host/mydb@Bean
@Profile("dev")
public DataSource devDataSource() { ... }
@Bean
@Profile("prod")
public DataSource prodDataSource() { ... }Spring Boot resolves properties in this order (highest priority first):
- Command-line arguments (
--server.port=9090) SPRING_APPLICATION_JSON(env variable / system property)@TestPropertySource(in tests)- System environment variables (
SERVER_PORT) application-{profile}.propertiesapplication.properties/application.yml@PropertySourceannotations on@Configurationclasses- Default properties (
SpringApplication.setDefaultProperties)
Q: "How does Spring Boot Auto-Configuration work?"
Spring Boot uses
@EnableAutoConfigurationwhich triggersAutoConfigurationImportSelector. It reads configuration classes fromMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. These classes are applied conditionally — based on classpath presence (@ConditionalOnClass), existing beans (@ConditionalOnMissingBean), and properties (@ConditionalOnProperty). If conditions match, Boot configures beans automatically. If the user defines a bean manually, Boot backs off. Starter dependencies work by pulling the right classes onto the classpath, which activates the corresponding auto-configurations.
Follow-up questions to expect:
- How would you write your own auto-configuration?
- What's the difference between
@Componentscanning and auto-configuration? - How do you debug which conditions passed or failed? →
/actuator/conditions - What changed in Spring Boot 3.x? →
AutoConfiguration.importsreplacedspring.factories