Boot Auto-Configuration & Startup
How a three-line main class becomes a running web service: starters shape the classpath, @SpringBootApplication scans your code and imports ~150 conditional configurations, @Conditional* annotations choose which survive, and SpringApplication.run() drives startup to a ready embedded Tomcat. Includes writing your own starter and reading the conditions report.
Why Spring Boot Exists
Before Boot, a Spring MVC + JPA service started with an afternoon of plumbing: pick compatible versions of 15 libraries, write a web.xml or WebApplicationInitializer, declare a DataSource, an EntityManagerFactory, a TransactionManager, a ViewResolver, a Jackson converter, then package a WAR and deploy it to a Tomcat someone else had installed. None of it was business logic and all of it was the same in every project.
Same running system: a pooled DataSource, an EntityManagerFactory, a transaction manager, Jackson-enabled MVC and an embedded Tomcat on port 8080. The code did not vanish — the same @Bean definitions now live inside the Boot jars as auto-configuration classes, and each one only activates when the classpath and your configuration say it should. Boot is a very large, very well-organised library of conditional @Configuration classes plus a launcher.
- Not a code generator — nothing is written into your source tree.
- Not a different framework — everything it does can be done with plain Spring
@Configurationclasses; you could copy any auto-config into your own project. - Not magic that runs at build time — all the decisions happen at startup by evaluating conditions against the classpath and
Environment.
Classpath In, Beans Out
spring-boot-starter-web, -data-jpa)@SpringBootApplication enables @EnableAutoConfigurationDeferredImportSelector reads a list of ~150 candidate configuration classes from META-INF/spring/...AutoConfiguration.imports in every jar.@Conditional* annotationsDispatcherServlet on the classpath? Is there already a DataSource bean? Is spring.datasource.url set? Non-matching classes are dropped.@Bean definitions after your own@ConditionalOnMissingBean backs off wherever you customised.SpringApplication.run() refreshes the contextEnvironment properties feed everything@ConfigurationProperties classes bind spring.datasource.*, server.*, … from yaml, env vars, CLI args (precedence order).Keep one picture: candidates in, conditions as filters, bean definitions out. When something is auto-configured that you did not expect — or something you expected is missing — the answer is always in one of three places: the classpath, the Environment, or a bean you already declared. Chapter 10 shows how to read the report that tells you which.
Starters & Dependency Management
A starter is a dependency descriptor with no code of its own. spring-boot-starter-web is an empty jar whose POM pulls in spring-boot-starter (core + logging + YAML), spring-webmvc, spring-boot-starter-json (Jackson) and spring-boot-starter-tomcat. Adding it changes the classpath, and the classpath changes what auto-configuration activates. That is the entire trick.
| Starter | Brings (main pieces) | Auto-config it unlocks |
|---|---|---|
spring-boot-starter-web | Spring MVC, Jackson, embedded Tomcat | DispatcherServlet, MVC defaults, Jackson ObjectMapper, Tomcat factory |
spring-boot-starter-data-jpa | Spring Data JPA, Hibernate, HikariCP, spring-tx | DataSource, EntityManagerFactory, JpaTransactionManager, repositories scanning |
spring-boot-starter-security | Spring Security | Default SecurityFilterChain (everything authenticated, form login + basic), generated password |
spring-boot-starter-actuator | Micrometer, actuator endpoints | /actuator/health, metrics registry |
spring-boot-starter-validation | Hibernate Validator | Method validation post-processor, @Valid support |
spring-boot-starter-aop | AspectJ weaver library | @EnableAspectJAutoProxy with CGLIB |
spring-boot-starter-test | JUnit, AssertJ, Mockito, Spring Test | Test slices, @SpringBootTest |
The other half is dependency management. spring-boot-starter-parent (or the spring-boot-dependencies BOM imported into your own parent) pins hundreds of library versions that are known to work together, which is why you write jackson-databind without a version. It also configures the compiler (-parameters), resource filtering and the repackage plugin.
- Overriding a managed version carelessly. Setting
<jackson.version>in properties is supported; hard-coding a singlejackson-databindversion producesNoSuchMethodError/NoClassDefFoundErrorat runtime because the other Jackson modules stay on the BOM version. Override the BOM property so all modules move together. - Two starters bring competing implementations (Tomcat + Jetty, Logback + Log4j2). Boot picks one deterministically, but logs a warning (
Class path contains multiple SLF4J providers); exclude the one you do not want. - A starter on the classpath you do not need activates its auto-config. Adding
spring-boot-starter-securityto experiment locks every endpoint behind a generated password:Using generated security password: 7f3e...in the log. - JDBC driver missing with
starter-data-jpa:Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.Add a driver (orh2) or setspring.datasource.url. - Spring Boot 3 requires Java 17+ and Jakarta EE 10:
javax.*imports from older libraries causeClassNotFoundException: javax.servlet...; migrate dependencies, do not mix.
@SpringBootApplication Taken Apart
@SpringBootApplication is not a special keyword — it is three ordinary annotations in a trench coat. Writing it out shows exactly where each behaviour comes from.
| Part | Does | Detail worth knowing |
|---|---|---|
@SpringBootConfiguration | Marks the class as a configuration source (specialisation of @Configuration) | Test slices search upward from the test package for exactly one @SpringBootConfiguration — why a missing/duplicate one gives Unable to find a @SpringBootConfiguration |
@ComponentScan | Scans the annotated class’s package and sub-packages for @Component/@Service/@Controller/@Configuration | No basePackages given → package of the class. A main class in the default package scans the whole classpath (never do it) |
TypeExcludeFilter | Lets test slices (@WebMvcTest) exclude components | Why @WebMvcTest does not load your services |
AutoConfigurationExcludeFilter | Keeps auto-configuration classes out of your scan | They must only load via @EnableAutoConfiguration so ordering and conditions apply |
@EnableAutoConfiguration | @AutoConfigurationPackage + @Import(AutoConfigurationImportSelector.class) | @AutoConfigurationPackage records your package so JPA/Mongo repositories and entities are found by default |
- Main class in a sub-package (
com.acme.app.Main) while services live incom.acme.orders→ nothing found:NoSuchBeanDefinitionException: No qualifying bean of type 'com.acme.orders.OrderService'. Put the main class in the root package. - Third-party library packages are not scanned. A
@Componentinside a jar underorg.thirdpartyis ignored.@Importthe class or expose it through its own auto-configuration. - Scanning picks up an auto-config class you wrote in your own tree: it ignores ordering/conditions and may register too early. Put auto-configs in a separate module, listed only in the imports file.
excludenaming a class not on the classpath fails withThe following classes could not be excluded because they are not auto-configuration classes— useexcludeNamefor optional types.@SpringBootApplicationon a class that also declares heavy@Beanmethods makes tests slower and sliced tests load them; keep it empty and move beans to@Configurationclasses.
AutoConfiguration.imports & Deferred Import
@EnableAutoConfiguration imports AutoConfigurationImportSelector. It is a DeferredImportSelector, which means Spring processes it last, after every one of your @Configuration classes and component scans has registered its beans. That ordering is what makes back-off possible: by the time Boot’s defaults are evaluated, your definitions are already in the registry.
| Mechanism | Used for | Boot 3.x status |
|---|---|---|
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | Listing auto-configuration classes | The way since 2.7; spring.factories auto-config entries are no longer read in Boot 3 |
META-INF/spring.factories | Other extension points: ApplicationContextInitializer, ApplicationListener, EnvironmentPostProcessor, FailureAnalyzer, … | Still supported for these |
@AutoConfiguration | Replaces @Configuration on an auto-config class (proxyBeanMethods=false built in) | Use for all new starters |
@AutoConfigureBefore/After/Order | Ordering between auto-configs (not vs your configs — those are always first) | Attributes before/after on @AutoConfiguration |
- Ordering confusion.
@AutoConfigureAfteronly orders among auto-configurations. Your own configs are always processed before; you cannot make a user config run after an auto-config by annotating it. - Putting the imports file in the wrong directory (
META-INF/spring.imports, or underresources/root withoutMETA-INF/spring/): silently ignored. Name must be exactlyorg.springframework.boot.autoconfigure.AutoConfiguration.imports. - Using
spring.factorieswithEnableAutoConfigurationkey in a Boot 3 starter — no longer honoured, the starter works on Boot 2.x and does nothing on 3.x. - Component-scanning an auto-config bypasses deferred import. Keep the package outside the app’s scan root.
- @Bean methods in auto-config should be non-
public-class-friendly: use@AutoConfiguration(no CGLIB subclass) and do not call one@Beanmethod from another — pass dependencies as parameters.
@Conditional Family & Back-Off
A @Conditional is a predicate on a configuration class or @Bean method: Condition.matches(ConditionContext, AnnotatedTypeMetadata). Boot ships a family of ready-made ones in org.springframework.boot.autoconfigure.condition. They are evaluated in two phases: PARSE_CONFIGURATION (can this config class even be read — used by class-level class/property checks) and REGISTER_BEAN (when each @Bean is about to be registered — used by @ConditionalOnBean because it must see what is already defined).
| Annotation | Matches when | Typical use |
|---|---|---|
@ConditionalOnClass / @ConditionalOnMissingClass | A class is / is not on the classpath (read via ASM metadata, so the class is never loaded — no NoClassDefFoundError) | Activate Jackson config only if Jackson exists |
@ConditionalOnBean / @ConditionalOnMissingBean | A bean of the type/name is / is not already registered | Back-off: provide a default unless the user did |
@ConditionalOnSingleCandidate | Exactly one bean of the type (or one @Primary) | JdbcTemplate needs one DataSource |
@ConditionalOnProperty | A property exists / equals a value (havingValue, matchIfMissing) | Feature flags: acme.cache.enabled=true |
@ConditionalOnWebApplication | Servlet / reactive / any web context | MVC config only in servlet apps |
@ConditionalOnResource | A file/classpath resource exists | Load banner.txt, logback.xml |
@ConditionalOnExpression | A SpEL expression is true | Composite flags (use sparingly) |
@ConditionalOnCloudPlatform / @ConditionalOnJava | Kubernetes/Cloud Foundry / JVM version | Platform-specific tuning |
@Profile | A Spring profile is active (a plain @Conditional) | Environment-specific beans |
@ConditionalOnBeanin your own configuration is order-dependent. It only sees beans registered before that point; it is designed for auto-configuration. In user code it works or not depending on scan order — use@Import/explicit ordering or restructure.@ConditionalOnMissingBeanwithout a type infers it from the@Beanreturn type. If you return a supertype or interface your own subtype bean may not count as "present". Declare the specific return type.@ConditionalOnPropertywithmatchIfMissingforgotten: flag absent → condition false → bean silently missing →NoSuchBeanDefinitionExceptionfar away. Decide the default explicitly.@ConditionalOnClassplaced on a@Beanmethod with a class argument that is absent: the method signature itself loads the class →NoClassDefFoundError. Put it on the class (or a nested config class) so ASM handles it.- Your
@Beanof the same name as an auto-config bean overrides or conflicts: Boot 2.1+ disallows bean definition overriding by default →BeanDefinitionOverrideException: Invalid bean definition with name 'x'. Rely on back-off by type instead of fighting names.
The SpringApplication.run() Sequence
SpringApplication.run(...) is a fixed choreography. Knowing the order tells you where each hook fits: where application.yml is read, when beans exist, when the port opens, and when your CommandLineRunner executes.
| Phase | What happens | Hook you can use |
|---|---|---|
| Construct | Reads META-INF/spring.factories for ApplicationContextInitializer and ApplicationListener; picks SERVLET / REACTIVE / NONE by classpath | new SpringApplicationBuilder(...), setBannerMode, setLazyInitialization |
| Environment | ApplicationEnvironmentPreparedEvent: EnvironmentPostProcessors add property sources; ConfigDataEnvironmentPostProcessor loads application.yml, profile files, spring.config.import | EnvironmentPostProcessor (register in spring.factories) |
| Context creation | Context type chosen from web type; bean factory created; sources (your main class) registered as a bean definition | ApplicationContextInitializer |
| refresh() | BeanFactoryPostProcessors run (ConfigurationClassPostProcessor parses configs, scans, imports auto-config); BeanPostProcessors registered; onRefresh() creates the web server; singletons instantiated | BeanFactoryPostProcessor, BeanPostProcessor, @PostConstruct |
| finishRefresh | SmartLifecycle beans start — including WebServerStartStopLifecycle, which starts the web server and publishes ServletWebServerInitializedEvent; then ContextRefreshedEvent | SmartLifecycle, @EventListener |
| Runners | ApplicationRunner / CommandLineRunner beans in @Order order; port is already open | Warm-up, seed data (keep short!) |
| Ready | ApplicationReadyEvent, then ReadinessState.ACCEPTING_TRAFFIC | @EventListener(ApplicationReadyEvent.class) |
| Failure | Any exception: ApplicationFailedEvent, FailureAnalyzers turn it into the readable "APPLICATION FAILED TO START" block, context closed, exit code 1 | FailureAnalyzer, ExitCodeGenerator |
- Doing slow work in a
CommandLineRunnerdelaysApplicationReadyEventand therefore readiness. The port is open but Kubernetes readiness is not yet ACCEPTING_TRAFFIC — that is correct behaviour; a runner that takes 5 minutes makes the pod flap. Use async warm-up or a startup probe. - Reading properties in a static initializer or constructor of
main: theEnvironmentdoes not exist yet. Use anEnvironmentPostProcessorif you must contribute properties early. - "Started in 4.3 seconds" is misleading. It is measured to just before runners. Time-to-ready must be measured separately (
ApplicationReadyEventtimestamp). - Failure analysis blocks hide the root cause: run with
--debugor look forCaused by:lines; for the common ones the analyzer prints Description and Action (e.g.Web server failed to start. Port 8080 was already in use.). spring.main.lazy-initialization=truespeeds startup but moves bean creation errors to the first request and changes@PostConstructtiming. Use consciously.
Embedded Tomcat & the Executable Jar
"Embedded" means your application contains the servlet container as a library, instead of being deployed into one. ServletWebServerFactoryAutoConfiguration imports a factory for whichever server is on the classpath (TomcatServletWebServerFactory, Jetty or Undertow). During onRefresh() the context asks that factory to create a WebServer and hands it a list of ServletContextInitializer beans — among them the DispatcherServletRegistrationBean that registers the Spring MVC DispatcherServlet at / and every Filter bean you declared.
| Property (default) | Controls |
|---|---|
server.port (8080) | Listen port; 0 picks a random free port (tests) |
server.tomcat.threads.max (200) | Max request-processing threads — the real concurrency limit for blocking MVC |
server.tomcat.threads.min-spare (10) | Threads kept ready |
server.tomcat.accept-count (100) | Queue of pending connections once all connections/threads are busy |
server.tomcat.max-connections (8192) | Max concurrently open connections the server accepts |
server.shutdown=graceful (default immediate) | Stop accepting, let in-flight requests finish up to spring.lifecycle.timeout-per-shutdown-phase (30s) |
spring.threads.virtual.enabled=true | Boot 3.2+ on Java 21: request handling on virtual threads |
java -jar app.jar starts the JarLauncher, which builds a classloader that can read nested jars, then calls Start-Class.main(). This is why ClassLoader.getResource on a file in BOOT-INF/classes works but new File("classpath:...") does not — there is no file on disk, only an entry inside a jar.
Web server failed to start. Port 8080 was already in use.Another process (or a previous instance) holds the port. Find it withss -ltnp | grep 8080; or useserver.port=0in tests.- Thread-pool starvation: all 200 Tomcat threads blocked on a slow downstream. Symptom: latency spikes while CPU is idle;
tomcat.threads.busyequalstomcat.threads.config.maxin metrics. Fix with timeouts/bulkheads, not just a bigger pool. - Dropped requests on deploy without
server.shutdown=gracefuland a matching KubernetespreStop/terminationGracePeriodSecondslarger than the shutdown timeout. - Both
spring-boot-starter-tomcatand-jettyon the classpath: Boot chooses by condition order (Tomcat wins) — your Jetty config is ignored. Exclude Tomcat explicitly. - Deploying the fat jar as a WAR into an external Tomcat needs
SpringBootServletInitializer,packaging warand the Tomcat starter asprovided; otherwise two containers start and fight over ports. - Reading files from
src/main/resourcesasFileworks in the IDE and fails in the jar (FileNotFoundException: class path resource ... cannot be resolved to absolute file path). UseClassPathResource.getInputStream().
Writing Your Own Starter
The best way to understand auto-configuration is to write one. We will build acme-spring-boot-starter: a tiny library that offers a GreetingService configured by acme.greeting.* properties, which backs off if the application declares its own, and can be switched off with a flag. Convention: third-party starters are named acme-spring-boot-starter (the spring-boot-starter-* prefix is reserved for Spring).
Usage from an application is then one dependency and zero code:
- Forgetting
@ConditionalOnMissingBean— users cannot customise; they getNoUniqueBeanDefinitionExceptionor have to exclude your auto-config. - Hard dependency on optional libraries. Mark them
<optional>true</optional>in the autoconfigure module and guard the config with@ConditionalOnClass, or the starter forces them on every consumer. - Eager side effects in
@Beanmethods (opening connections) — auto-config runs for every consumer at startup; make beans lazy-friendly and fail with a clear message. - Property names without metadata: add
spring-boot-configuration-processor(annotation processor) so IDEs autocompleteacme.greeting.*and flag typos. - Testing only with
@SpringBootTestis slow and hides condition bugs.ApplicationContextRunnerstarts a minimal context per test and lets you vary classpath (withClassLoader(new FilteredClassLoader(...))) and properties.
Debugging Auto-Configuration
Almost every Boot mystery is a condition answering differently than you assumed. Boot records every condition outcome in a ConditionEvaluationReport; you only need to ask for it.
| Tool | How | Gives you |
|---|---|---|
--debug / debug=true | java -jar app.jar --debug | Logs the full CONDITIONS EVALUATION REPORT at startup (also enables DEBUG for core loggers) |
/actuator/conditions | expose management.endpoints.web.exposure.include=conditions | The same report as JSON on a running app |
/actuator/beans | expose beans | Every bean, its type, scope, dependencies |
/actuator/configprops, /actuator/env | expose them | Bound property values and which property source supplied them |
logging.level.org.springframework.boot.autoconfigure=DEBUG | property | Per-condition trace without the whole debug firehose |
ApplicationContextRunner | in tests | Reproduce a condition outcome in isolation |
BufferingApplicationStartup + /actuator/startup | app.setApplicationStartup(new BufferingApplicationStartup(2048)) | Timing per startup step — find the slow bean |
Read it with two questions. For something unexpectedly present, search Positive matches for its auto-config and read which classpath/property fact satisfied it, then remove the cause (starter, property) or add exclude. For something unexpectedly absent, search Negative matches for it and read the first line under Did not match — that is the failing condition, and it names the missing class, bean or property.
- Exposing
/actuator/conditions,/beans,/envpublicly leaks classpath and configuration (values are sanitized, but structure is not). Put the management port behind the network boundary or secure the endpoints. - Unexpected Spring Security lock-down after adding a dependency (transitive
spring-security-*from another starter): search Positive matches forSecurityAutoConfiguration/UserDetailsServiceAutoConfiguration. - "Why is my bean not injected?" is usually not Boot at all: check component-scan reach (Chapter 4) before diving into conditions.
- Using
--debugin production floods logs; use the actuator endpoint or run once locally with the production classpath. - Slow startup: do not guess — record with
BufferingApplicationStartupand sort steps by duration; typical culprits are Hibernate schema validation, huge component scans and eager remote calls in@PostConstruct.
A Mini Auto-Configuration Engine
Here is the whole idea in about 80 lines of plain Java — no Spring: user beans registered first, an ordered list of auto-config candidates, each with an ordered list of conditions (the first failing one is recorded, as the report does), and a registry that supports missing bean of type. Compile and run it.
Reading the output as Boot would: the app declared its own DataSource (myCustomDs), so Boot’s Hikari one backed off; the JdbcTemplate candidate passed all three conditions (class present, flag true, none defined); Jackson was absent from this bare javac classpath, and Kafka likewise. This is literally the structure of the real report, minus ASM metadata, ordering and bean-factory machinery. Note the evaluation order inside a candidate — Boot also orders cheap checks (OnClass) before costly ones (OnBean).
- Add
reg.beans.put("jdbcTemplate", ...)before evaluation and confirm the template candidate flips to Negative. - Put
onClass("java.sql.Connection")last in a candidate’s list: the report now blames a different condition when several fail. Boot evaluates and reports in declaration order too, so keep specific conditions first.
Interview Angles
- What does
@SpringBootApplicationdo?
Meta-annotation for@SpringBootConfiguration(a@Configuration),@ComponentScan(from the class’s package down, with test/auto-config exclude filters) and@EnableAutoConfiguration. - How does auto-configuration actually find its classes?
AutoConfigurationImportSelector(aDeferredImportSelector) readsMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsfrom all jars, filters/sorts them, and imports each as a normal configuration. In Boot 3spring.factoriesis no longer used for this. - Why do your own beans override Boot’s defaults?
Auto-config is imported after user configuration (deferred) and guarded with@ConditionalOnMissingBean; so when yours exists, the condition is false and the default is skipped. - Name five
@Conditional*annotations and when each is evaluated.
OnClass, OnMissingBean, OnProperty, OnWebApplication, OnResource. Class-level class/property checks at config parsing; bean-based ones at bean registration (REGISTER_BEAN phase). - What is a starter?
A POM-only artifact that groups compatible dependencies (and often an autoconfigure module). It works because the extra jars on the classpath trigger conditional configuration. - Walk through
SpringApplication.run().
Construct (web type, factories) → environment prepared (yaml, profiles) → create context → prepare (initializers) → refresh (config parsing, auto-config, bean creation, server created) → finishRefresh (server started) → Started event → runners → Ready event. - When is the HTTP port actually opened?
Server object is created inonRefresh()but connectors are only bound whenWebServerStartStopLifecyclestarts infinishRefresh— after all singletons exist. - How do you find out why a bean exists or doesn’t?
--debugor/actuator/conditionsand read Positive/Negative matches;/actuator/beansfor the resulting graph. - How do you disable an auto-configuration?
exclude/excludeNameon the annotation orspring.autoconfigure.exclude; or change the condition input (remove the starter, set the property). - How would you swap Tomcat for Jetty or Undertow?
Excludespring-boot-starter-tomcatfromstarter-weband addspring-boot-starter-jetty/-undertow; the factory auto-config switches on the classpath. - What is in an executable Boot jar and how does it start?
BOOT-INF/classes+BOOT-INF/libnested jars;Main-ClassisJarLauncher,Start-Classis your main class.
Summary & Cheat Sheet
| Stage | Mechanism | Where to look when wrong |
|---|---|---|
| Dependencies | Starters + BOM (spring-boot-starter-parent) | mvn dependency:tree |
| Discovery | @SpringBootApplication scan from main-class package; imports file for auto-config | Package layout; imports file path/name |
| Selection | @Conditional* on candidates (class, bean, property, web type) | --debug report, /actuator/conditions |
| Customisation | Your beans back off defaults; @ConfigurationProperties tweaks them | Bean type vs return type; property prefix |
| Startup | SpringApplication.run: env → context → refresh → server start → runners → ready | Startup log, BufferingApplicationStartup |
| Packaging | Fat jar + JarLauncher; embedded Tomcat | jar tf app.jar, server.* properties |
- Boot is a catalogue of conditional configurations; starters change the classpath that conditions inspect.
@SpringBootApplication= configuration + component scan (your package tree) + auto-configuration.- Auto-config classes come from
AutoConfiguration.imports, imported after yours via a deferred selector. @ConditionalOnMissingBeanis the back-off contract: declare your own bean and Boot’s steps aside.run(): environment first, beans during refresh, server starts at finishRefresh, runners, then Ready.- The executable jar is just a launcher + nested jars; Tomcat is a library inside your process.
- Always debug with the conditions report before changing code.
- 01 IoC Container & DI — the container that evaluates all of this.
- 02 Bean Scopes & Lifecycle —
BeanFactoryPostProcessor/BeanPostProcessorphases insiderefresh(). - 11 Configuration & Profiles — property sources,
@ConfigurationProperties, profiles in depth. - 12 Actuator & Observability — conditions/beans/startup endpoints and health.