Spring Boot Internals Deep Dive
Understand what happens under the hood — from
SpringApplication.run()to a fully running application serving requests.
Real-World Analogy
Think of Spring Boot startup like launching a spacecraft. Mission Control (SpringApplication) runs through a precise checklist: check environment (fuel, weather), assemble components (payload, boosters), configure systems (navigation, comms), verify everything works together (integration checks), then ignite engines (start embedded server). Each phase must complete before the next begins, and any failure triggers an abort sequence.
The Startup Process
When you call SpringApplication.run(MyApp.class, args), here's the precise sequence:
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
A["main() →<br/>SpringApplication.run()"] --> B["1. Create<br/>SpringApplication"]
B --> C["2. Detect<br/>WebApplicationType"]
C --> D["3. Load<br/>RunListeners"]
D --> E["4. Prepare<br/>Environment"]
E --> F["5. Print Banner"]
F --> G["6. Create<br/>ApplicationContext"]
G --> H["7. Prepare Context"]
H --> I["8. Refresh Context<br/>⭐ beans created"]
I --> J["9. Start Server<br/>(Tomcat/Jetty)"]
J --> K["10. Call Runners"]
K --> L["Application Ready"]
style A fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
style B fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style C fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style D fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style E fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style F fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style G fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style H fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style I fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
style J fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style K fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style L fill:#D1FAE5,stroke:#6EE7B7,color:#1E40AF Phase Breakdown
Phase 1: SpringApplication Initialization
// What happens in new SpringApplication(primarySources)
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// Detects if you have Servlet or Reactive on classpath
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// Loads from META-INF/spring.factories
this.bootstrapRegistryInitializers = getBootstrapRegistryInitializers();
setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners(getSpringFactoriesInstances(ApplicationListener.class));
// Finds the class with main() via stack trace inspection
this.mainApplicationClass = deduceMainApplicationClass();
}
WebApplicationType Detection
Spring Boot checks your classpath to determine the application type:
SERVLET— ifDispatcherServletandServletContainerare presentREACTIVE— ifDispatcherHandleris present but NOTDispatcherServletNONE— neither (CLI app, batch job)
Phase 2: Environment Preparation
The Environment is assembled in a specific property source order (later sources override earlier):
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | Command line args | --server.port=9090 |
| 2 | JNDI attributes | java:comp/env |
| 3 | System properties | -Dserver.port=9090 |
| 4 | OS environment vars | SERVER_PORT=9090 |
| 5 | application-{profile}.yml | application-prod.yml |
| 6 | application.yml | Default config |
| 7 | @PropertySource annotations | Custom property files |
| 8 (lowest) | Default properties | SpringApplication.setDefaultProperties() |
Phase 3: Context Refresh (The Big One)
The refresh() method in AbstractApplicationContext is where 90% of the work happens:
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
subgraph Refresh["AbstractApplicationContext.refresh()"]
direction TB
R1["prepareRefresh()"] --> R2["obtainFreshBeanFactory()"]
R2 --> R3["prepareBeanFactory()"]
R3 --> R4["postProcessBeanFactory()"]
R4 --> R5["invokeBeanFactoryPostProcessors()<br/>⭐ Auto-config happens here"]
R5 --> R6["registerBeanPostProcessors()"]
R6 --> R7["initMessageSource()"]
R7 --> R8["initApplicationEventMulticaster()"]
R8 --> R9["onRefresh()<br/>⭐ Embedded server created"]
R9 --> R10["registerListeners()"]
R10 --> R11["finishBeanFactoryInitialization()<br/>⭐ All singleton beans created"]
R11 --> R12["finishRefresh()"]
end
style R5 fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
style R9 fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
style R11 fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF Auto-Configuration Machinery
How Spring Boot "Guesses" What You Need
Auto-configuration is not magic — it's a well-defined loading mechanism with conditional evaluation.
Case Study: DataSource Auto-Configuration
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart TD
START["Spring Boot Starts"] --> SCAN["Scan AutoConfiguration.imports<br/>finds DataSourceAutoConfiguration"]
SCAN --> C1{"@ConditionalOnClass<br/>javax.sql.DataSource<br/>on classpath?"}
C1 -->|"No (no JDBC driver)"| SKIP["SKIP: DataSource not configured"]
C1 -->|"Yes (H2/PostgreSQL/MySQL driver found)"| C2{"@ConditionalOnMissingBean<br/>User defined custom DataSource?"}
C2 -->|"Yes (user has @Bean DataSource)"| BACK["BACK OFF: Use user's bean"]
C2 -->|"No (no custom bean)"| C3{"spring.datasource.url<br/>configured?"}
C3 -->|"Yes"| POOL["Create HikariCP DataSource<br/>with configured URL"]
C3 -->|"No"| EMBED["Create Embedded DB<br/>(H2 in-memory)"]
POOL --> DONE["DataSource bean ready"]
EMBED --> DONE
DONE --> JPA{"HibernateJpaAutoConfiguration<br/>@ConditionalOnBean(DataSource)"}
JPA -->|"DataSource exists"| HIBERNATE["Configure Hibernate + EntityManager"]
style START fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
style SKIP fill:#FEE2E2,stroke:#FCA5A5,color:#1E40AF
style BACK fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
style DONE fill:#D1FAE5,stroke:#6EE7B7,color:#1E40AF
style POOL fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style EMBED fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style HIBERNATE fill:#D1FAE5,stroke:#6EE7B7,color:#1E40AF
style C1 fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style C2 fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style C3 fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style JPA fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF Read the diagram above carefully
This is exactly how Spring Boot decides whether to auto-configure your database. The key insight: your custom beans always win because of @ConditionalOnMissingBean. Spring Boot only provides defaults when you haven't defined your own.
Loading Mechanism (Spring Boot 3.x)
This file lists auto-configuration classes, one per line:
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
Spring Boot 2.x vs 3.x
In Spring Boot 2.x, auto-configurations were listed in META-INF/spring.factories under the EnableAutoConfiguration key. Spring Boot 3.x uses the new .imports file format. The old mechanism still works but is deprecated.
The @Conditional Family
Every auto-configuration class is wrapped in conditions. Spring Boot evaluates these BEFORE loading the class:
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass(EntityManager.class) // Only if JPA is on classpath
@ConditionalOnBean(DataSource.class) // Only if a DataSource exists
@ConditionalOnProperty(prefix = "spring.jpa", name = "enabled", matchIfMissing = true)
public class HibernateJpaAutoConfiguration {
// Only loaded if ALL conditions pass
}
| Annotation | Evaluates To True When... |
|---|---|
@ConditionalOnClass | Specified class is on classpath |
@ConditionalOnMissingClass | Specified class is NOT on classpath |
@ConditionalOnBean | Specified bean already exists in context |
@ConditionalOnMissingBean | Specified bean does NOT exist (key one!) |
@ConditionalOnProperty | Property has specified value |
@ConditionalOnResource | Resource (file) exists on classpath |
@ConditionalOnWebApplication | Running in a web context |
@ConditionalOnExpression | SpEL expression evaluates to true |
The Golden Rule
@ConditionalOnMissingBean is why your custom beans always win. If you define your own DataSource bean, the auto-configured one backs off because the condition fails.
Debugging Auto-Configuration
# Option 1: Start with --debug flag
java -jar myapp.jar --debug
# Option 2: Add to application.yml
debug: true
# Option 3: Actuator endpoint (if actuator is included)
# GET /actuator/conditions
The debug output shows:
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'
Negative matches:
-----------------
MongoAutoConfiguration:
- @ConditionalOnClass did not find required class 'com.mongodb.client.MongoClient'
Class Loading & Fat JAR
The Executable JAR Structure
When you run mvn package, Spring Boot creates a "fat JAR" with a special structure:
my-app-1.0.0.jar
├── META-INF/
│ └── MANIFEST.MF → Main-Class: JarLauncher (not your class!)
├── BOOT-INF/
│ ├── classes/ → YOUR compiled code
│ │ └── com/example/...
│ ├── lib/ → ALL dependency JARs (nested!)
│ │ ├── spring-web-6.1.0.jar
│ │ ├── tomcat-embed-core-10.1.jar
│ │ └── ... (hundreds of JARs)
│ └── classpath.idx → Classpath ordering
└── org/springframework/boot/loader/
├── JarLauncher.class → The real entry point
└── launch/
└── LaunchedURLClassLoader.class
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart TD
subgraph JAR["Executable JAR"]
JL["JarLauncher<br/>(real Main-Class)"]
JL --> CL["LaunchedURLClassLoader<br/>(custom ClassLoader)"]
CL --> BC["BOOT-INF/classes/<br/>(your code)"]
CL --> BL["BOOT-INF/lib/<br/>(dependency JARs)"]
BL --> MA["Your @SpringBootApplication<br/>(actual main method)"]
end
style JL fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
style CL fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
style MA fill:#D1FAE5,stroke:#6EE7B7,color:#1E40AF Why JarLauncher?
Standard Java cannot load classes from JARs nested inside JARs. The LaunchedURLClassLoader solves this by implementing a custom URL protocol (jar:nested:) that reads classes from nested JARs without extracting them to disk.
Bean Lifecycle Internals
The 11 Steps of Bean Creation
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
B1["1. Definition<br/>Loaded"] --> B2["2. BeanFactory<br/>PostProcessor"]
B2 --> B3["3. Instantiation"]
B3 --> B4["4. Populate<br/>Properties"]
B4 --> B5["5. BeanName<br/>Aware"]
B5 --> B6["6. BeanFactory<br/>Aware"]
B6 --> B7["7. AppContext<br/>Aware"]
B7 --> B8["8. BPP Before<br/>(@PostConstruct)"]
B8 --> B9["9. afterProperties<br/>Set / init-method"]
B9 --> B10["10. BPP After<br/>⭐ AOP Proxies"]
B10 --> B11["11. Bean Ready"]
style B1 fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
style B2 fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
style B8 fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
style B10 fill:#FEE2E2,stroke:#FCA5A5,color:#1E40AF
style B11 fill:#D1FAE5,stroke:#6EE7B7,color:#1E40AF BeanFactoryPostProcessor vs BeanPostProcessor
| Aspect | BeanFactoryPostProcessor | BeanPostProcessor |
|---|---|---|
| When | Before any bean is created | During each bean's creation |
| What it sees | Bean definitions (metadata) | Actual bean instances |
| Use case | Modify property values, add definitions | Wrap beans in proxies, validate |
| Example | PropertySourcesPlaceholderConfigurer | AutowiredAnnotationBeanPostProcessor |
| How many times called | Once (on the factory) | Once per bean |
Proxy Creation Decision
// Spring's decision logic (simplified)
if (beanClass.implementsAnyInterface()) {
// JDK Dynamic Proxy — proxy implements the interface
// Your bean is accessed through the interface type
return Proxy.newProxyInstance(interfaces, handler);
} else {
// CGLIB Proxy — generates a subclass at runtime
// Works with concrete classes (no interface needed)
return enhancer.create(); // MyService$$EnhancerBySpringCGLIB
}
CGLIB Limitation
CGLIB proxies cannot intercept final methods or private methods. If your @Transactional method is final, the transaction advice is silently skipped!
Building a Custom Starter
Project Structure
notification-spring-boot-starter/
├── src/main/java/com/example/notification/
│ ├── NotificationAutoConfiguration.java
│ ├── NotificationProperties.java
│ ├── NotificationService.java
│ └── NotificationTemplate.java
├── src/main/resources/
│ ├── META-INF/
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ └── META-INF/
│ └── additional-spring-configuration-metadata.json
└── pom.xml
Step 1: Configuration Properties
@ConfigurationProperties(prefix = "notification")
public class NotificationProperties {
private String provider = "smtp"; // default provider
private String from = "noreply@app.com";
private int retryAttempts = 3;
private Duration timeout = Duration.ofSeconds(5);
// getters and setters
}
Step 2: Auto-Configuration Class
@AutoConfiguration
@ConditionalOnClass(NotificationService.class)
@EnableConfigurationProperties(NotificationProperties.class)
public class NotificationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public NotificationTemplate notificationTemplate(NotificationProperties props) {
return new NotificationTemplate(props.getProvider(), props.getFrom());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "notification", name = "enabled", havingValue = "true", matchIfMissing = true)
public NotificationService notificationService(NotificationTemplate template) {
return new NotificationService(template);
}
}
Step 3: Register Auto-Configuration
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.notification.NotificationAutoConfiguration
Step 4: Configuration Metadata (IDE support)
{
"properties": [
{
"name": "notification.provider",
"type": "java.lang.String",
"description": "Notification provider (smtp, sns, twilio)",
"defaultValue": "smtp"
},
{
"name": "notification.enabled",
"type": "java.lang.Boolean",
"description": "Enable/disable notification auto-configuration",
"defaultValue": true
}
]
}
Naming Convention
Official Spring starters: spring-boot-starter-{name} (e.g., spring-boot-starter-web).
Third-party starters: {name}-spring-boot-starter (e.g., mybatis-spring-boot-starter).
Interview Questions
Q1: What happens if you put @SpringBootApplication on a class that's NOT in the root package?
Answer: @SpringBootApplication includes @ComponentScan which, by default, scans the package of the annotated class and all sub-packages. If your main class is in com.example.config but your services are in com.example.service, they won't be found.
Fix: Either move the main class to com.example (root), or explicitly set @ComponentScan(basePackages = "com.example").
Why interviewers ask this: It tests whether you understand the scanning mechanics vs just knowing annotations exist.
Q2: Why does calling a @Transactional method from within the same class not start a transaction?
Answer: Spring's @Transactional works via AOP proxies. When you call this.methodB() from methodA() within the same class, you bypass the proxy — you're calling the actual target object directly. The proxy interceptor never fires, so no transaction is created.
Fix: Inject the bean into itself (via @Lazy or ObjectProvider), extract to a separate class, or use AopContext.currentProxy() (not recommended).
Q3: How does Spring Boot decide between JDK Dynamic Proxy and CGLIB?
Answer: Since Spring Boot 2.0, CGLIB is the default (spring.aop.proxy-target-class=true). JDK Dynamic Proxies are used only when explicitly configured AND the bean implements an interface. CGLIB generates a subclass, so it works with concrete classes.
Key gotcha: CGLIB cannot proxy final classes or intercept final methods. Spring will silently skip AOP advice on final methods.
Q4: What is the difference between @AutoConfiguration and @Configuration?
Answer: @AutoConfiguration (Spring Boot 3.x) is specifically for auto-configuration classes. Unlike @Configuration:
- It supports ordering via
beforeandafterattributes - It's loaded from the
.importsfile, not component scanning - It's processed AFTER user-defined
@Configurationclasses (so@ConditionalOnMissingBeanworks correctly)
Q5: How would you debug a bean that's not being auto-configured?
Answer: Systematic approach:
- Run with
--debugand check the CONDITIONS EVALUATION REPORT - Look for the auto-configuration class in "Negative matches"
- Read which
@Conditionalfailed (usuallyOnClassorOnMissingBean) - Verify the dependency is on the classpath (
mvn dependency:tree) - Check if another configuration is defining the bean first (causing
OnMissingBeanto fail) - Use
/actuator/conditionsat runtime for a live view
Q6: Explain the difference between BeanFactoryPostProcessor and BeanPostProcessor with a real example.
Answer:
- BeanFactoryPostProcessor operates on bean definitions before any bean exists. Example:
PropertySourcesPlaceholderConfigurerresolves${...}placeholders in bean definitions. - BeanPostProcessor operates on bean instances after creation. Example:
AutowiredAnnotationBeanPostProcessorinjects@Autowireddependencies.
Key insight: If you need to modify what beans will be created or change their metadata → BFPP. If you need to wrap or modify actual bean objects → BPP.