Spring Boot β Internals & Architecture
A deep dive into how Spring Boot works under the hood: auto-configuration mechanics, the embedded server model, conditional bean loading, custom starters, and the event-driven architecture.
Auto-Configuration Deep Diveβ
How Auto-Configuration Worksβ
When @EnableAutoConfiguration is present (included in @SpringBootApplication), Spring Boot:
- Reads
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x) orMETA-INF/spring.factories(2.x) from every JAR on the classpath - Loads the listed auto-configuration classes
- Evaluates conditional annotations on each class
- Registers beans only if conditions are met
Classpath has HikariCP + PostgreSQL driver
β DataSourceAutoConfiguration conditions pass
β HikariDataSource bean created
β JdbcTemplate bean created
Key Conditional Annotationsβ
| Annotation | Bean Is Created When⦠|
|---|---|
@ConditionalOnClass | A specific class is on the classpath |
@ConditionalOnMissingBean | No bean of that type already exists |
@ConditionalOnProperty | A property has a specific value |
@ConditionalOnBean | A specific bean already exists in the context |
@ConditionalOnMissingClass | A specific class is NOT on the classpath |
@ConditionalOnWebApplication | The app is a web application |
@ConditionalOnExpression | A SpEL expression evaluates to true |
Example: DataSource Auto-Configurationβ
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Configuration
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.url")
static class PooledDataSourceConfiguration {
// Creates HikariCP DataSource with properties from application.yml
}
}
What this means: Spring Boot only creates a DataSource if:
DataSourceclass is on the classpath β- No R2DBC
ConnectionFactoryexists β - No custom
DataSourcebean is already defined β spring.datasource.urlproperty is set β
Overriding Auto-Configurationβ
Define your own bean, and auto-configuration backs off:
@Configuration
public class CustomDataSourceConfig {
@Bean
public DataSource dataSource() {
// Your custom DataSource β auto-config won't create one
return new CustomPoolDataSource();
}
}
You can also exclude specific auto-configurations:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
Embedded Server Architectureβ
How It Worksβ
Traditional Java web apps are packaged as WAR files and deployed to an external Tomcat/JBoss/WebLogic. Spring Boot flips this model:
Traditional: App β WAR β External Server
Spring Boot: App + Server β Fat JAR β java -jar
Spring Boot embeds the server inside the application:
ServletWebServerAutoConfigurationdetects servlet container on the classpath- Creates an
EmbeddedServletContainerFactory(e.g.,TomcatServletWebServerFactory) - Starts the server during
ApplicationContextrefresh - Registers
DispatcherServletprogrammatically
Server Optionsβ
| Server | Starter | Use Case |
|---|---|---|
| Tomcat | Default (included in starter-web) | General purpose, most widely used |
| Jetty | spring-boot-starter-jetty | Lightweight, good for async/WebSocket |
| Undertow | spring-boot-starter-undertow | High performance, non-blocking |
Switching servers β exclude Tomcat, add the alternative:
<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-undertow</artifactId>
</dependency>
Spring Boot Starter Mechanismβ
What Is a Starter?β
A starter is a dependency descriptor β a POM with no code, only managed transitive dependencies. It ensures compatible versions across libraries.
Anatomy of a Starterβ
spring-boot-starter-data-jpa
βββ spring-boot-starter (core)
βββ spring-boot-starter-aop
βββ spring-data-jpa
βββ hibernate-core
βββ jakarta.persistence-api
βββ spring-orm
βββ spring-aspects
Creating a Custom Starterβ
Custom starters follow a naming convention: {project}-spring-boot-starter.
Structure:
my-service-spring-boot-starter/
βββ src/main/java/
β βββ com/example/autoconfigure/
β βββ MyServiceAutoConfiguration.java
β βββ MyServiceProperties.java
βββ src/main/resources/
βββ META-INF/
βββ spring/
βββ org.springframework.boot.autoconfigure.AutoConfiguration.imports
Auto-configuration class:
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyServiceProperties properties) {
return new MyService(properties.getEndpoint(), properties.getTimeout());
}
}
Properties class:
@ConfigurationProperties(prefix = "my.service")
public class MyServiceProperties {
private String endpoint = "http://localhost:8080";
private int timeout = 5000;
// getters and setters
}
Registration file (AutoConfiguration.imports):
com.example.autoconfigure.MyServiceAutoConfiguration
Spring Boot Event Systemβ
Spring Boot publishes events during the application lifecycle. You can hook into these for custom initialization, logging, or cleanup.
Application Lifecycle Events (in order)β
| Event | When It Fires |
|---|---|
ApplicationStartingEvent | Before anything β just after run() is called |
ApplicationEnvironmentPreparedEvent | Environment is ready, context not yet created |
ApplicationContextInitializedEvent | Context created, beans not yet loaded |
ApplicationPreparedEvent | Beans loaded, context not yet refreshed |
ContextRefreshedEvent | Context fully refreshed, all beans instantiated |
ApplicationStartedEvent | Context refreshed, runners not yet called |
ApplicationReadyEvent | Everything ready β app can serve traffic |
ApplicationFailedEvent | Startup failed with an exception |
Listening to Eventsβ
@Component
public class ReadinessListener {
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
// Initialize caches, warm up connections, etc.
}
}
For events before the context is ready, register via SpringApplication:
SpringApplication app = new SpringApplication(MyApp.class);
app.addListeners(event -> {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
// Modify environment before context is created
}
});
app.run(args);
Configuration Properties Bindingβ
Spring Boot can bind structured properties to Java objects:
app:
cache:
enabled: true
ttl: 300
max-size: 1000
@ConfigurationProperties(prefix = "app.cache")
public class CacheProperties {
private boolean enabled;
private int ttl;
private int maxSize;
// getters and setters
}
// Or using Java Records (Spring Boot 3.x Constructor Binding):
@ConfigurationProperties(prefix = "app.cache")
public record CacheProperties(boolean enabled, int ttl, int maxSize) {}
Binding features:
- Relaxed binding β
max-size,maxSize,MAX_SIZEall map tomaxSize - Type conversion β Strings to
Duration,DataSize, enums, etc. - Validation β Add
@Validatedand use JSR-303 annotations (@NotNull,@Min, etc.) - Nested objects β Complex hierarchies bind naturally
- List/Map support β YAML lists and maps bind to
List<>andMap<>
Fat JAR Structureβ
Spring Boot packages everything into an executable JAR:
my-app.jar
βββ BOOT-INF/
β βββ classes/ β Your compiled code
β βββ lib/ β All dependency JARs
β βββ classpath.idx β JAR loading order
βββ META-INF/
β βββ MANIFEST.MF β Main-Class: JarLauncher
βββ org/springframework/boot/loader/
βββ JarLauncher.class β Entry point
βββ ... β Custom classloader
How it boots:
- JVM calls
JarLauncher.main()(specified in MANIFEST.MF) JarLaunchersets up a customClassLoaderthat can read nested JARs- Delegates to your
@SpringBootApplicationclass'smain()
Proxy Mechanism: CGLIB vs JDK Dynamic Proxyβ
Every Spring bean with @Transactional, @Async, @Cacheable, or similar AOP annotations is wrapped in a proxy object at startup. Understanding which proxy mechanism is chosen is critical for debugging and for senior architecture decisions.
Which Proxy Is Used?β
| Scenario | Proxy Type |
|---|---|
Bean implements an interface + proxyTargetClass=false | JDK Dynamic Proxy |
Bean has no interface OR proxyTargetClass=true (Spring Boot default) | CGLIB subclass proxy |
@Configuration classes | CGLIB (always, to intercept @Bean method calls) |
# Spring Boot default β forces CGLIB even for beans with interfaces
spring:
aop:
proxy-target-class: true # default
CGLIB Constraints You Must Knowβ
// β CGLIB cannot proxy final classes
@Service
public final class PaymentService {
@Transactional
public void pay() { ... } // Spring will throw at startup
}
// β CGLIB cannot proxy final methods β silently ignored
@Service
public class OrderService {
@Transactional
public final void place(Order order) { ... } // @Transactional has no effect
}
// β CGLIB requires a no-arg constructor (or no explicit constructors)
@Service
public class MyService {
public MyService(String param) { ... } // Fails with CGLIB β add no-arg constructor
}
Why @Configuration Always Uses CGLIBβ
@Configuration
public class AppConfig {
@Bean
public ServiceA serviceA() {
return new ServiceA(serviceB()); // calls serviceB() method directly
}
@Bean
public ServiceB serviceB() {
return new ServiceB();
}
}
// Without CGLIB: serviceB() would be called twice β two different instances
// With CGLIB proxy: serviceB() is intercepted β same singleton returned both times
// This is why @Configuration classes cannot be final!
ImportBeanDefinitionRegistrarβ
For framework-level programmatic bean registration β the mechanism used internally by Spring Data JPA's @EnableJpaRepositories and Spring Security's @EnableWebSecurity.
public class DynamicRepositoryRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// Called at configuration phase β before any beans are created
// Read annotation attributes from the importing class
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableDynamicRepositories.class.getName());
String basePackage = (String) attrs.get("basePackage");
// Programmatically register beans
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(DynamicRepositoryFactory.class)
.addConstructorArgValue(basePackage);
registry.registerBeanDefinition("dynamicRepositoryFactory", builder.getBeanDefinition());
}
}
// Usage β triggers the registrar
@Configuration
@Import(DynamicRepositoryRegistrar.class)
public class AppConfig { }
When to use: When you need to programmatically register beans based on classpath scanning, annotation attributes, or external configuration β similar to how Spring Data automatically creates repository implementations.
Profiles Architectureβ
Profiles control which beans and configurations are active:
@Configuration
@Profile("production")
public class ProductionConfig {
@Bean
public DataSource dataSource() {
// Production connection pool
}
}
@Configuration
@Profile("development")
public class DevConfig {
@Bean
public DataSource dataSource() {
// In-memory H2 for development
}
}
Profile resolution order:
spring.profiles.activefrom command linespring.profiles.activefrom environment variablespring.profiles.activefromapplication.propertiesspring.profiles.default(defaults to"default")
Profile-specific property files are loaded automatically:
application-dev.ymlwhendevprofile is activeapplication-prod.ymlwhenprodprofile is active- Values in profile-specific files override
application.yml
Summaryβ
Spring Boot's power comes from:
- Conditional auto-configuration that reacts to the classpath
- Embedded servers that simplify deployment
- Starter POMs that ensure dependency compatibility
- A rich event system for lifecycle hooks
- Relaxed property binding for type-safe configuration
- Fat JAR packaging for single-artifact deployment
Understanding these internals enables you to debug startup issues, write custom starters, and optimize application behavior.
Advanced Editorial Pass: Spring Boot Internals for Debuggabilityβ
Why Internals Matter in Real Systemsβ
- Understanding condition evaluation prevents accidental behavior changes during upgrades.
- Context lifecycle clarity improves startup sequencing and shutdown safety.
- Bean wiring visibility reduces time-to-diagnosis for production misconfiguration.
Failure Modesβ
- Relying on implicit ordering for critical initialization logic.
- Hidden bean replacement through permissive component scanning.
- Configuration binding mismatches that fail late under specific profiles.
Practical Heuristicsβ
- Keep package boundaries explicit for component scanning and auto-configuration imports.
- Surface condition and binding diagnostics in non-production environments.
- Add smoke tests for key context invariants after dependency upgrades.
Compare Nextβ
Interview Questionsβ
Q: How do you debug unexpected auto-configuration in production?β
A: Inspect condition evaluation reports, bean definitions, and classpath differences between environments.
Q: What is the practical difference between configuration properties and ad-hoc @Value usage?β
A: Properties classes provide typed, validated, maintainable configuration models; @Value is best for isolated simple values.
Q: Why are conditional annotations central to Boot internals?β
A: They control feature activation by classpath, properties, and existing beans, which is the core of Boot's behavior.
Q: When should a team build a custom starter?β
A: When repeated internal platform patterns need standardized auto-configured setup across many services.
Q: What failure mode appears when classpath changes silently?β
A: Different auto-configurations activate and alter runtime behavior without code changes.
Q: How do you prevent premature bean initialization bugs?β
A: Keep post-processors minimal, avoid heavy dependencies in early lifecycle hooks, and add startup invariants tests.
Q: Why does understanding fat JAR classloading matter operationally?β
A: It helps diagnose startup failures, shading conflicts, and environment-specific classpath issues quickly.