Spring Boot is not a new framework. It is Spring, plus a set of decisions made for you. Knowing which decisions, and how to see them, is the difference between using Boot and fighting it.
Starters
A starter is a dependency that contains no code. Its entire job is to depend on other things.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>That one line brings in Spring MVC, Jackson for JSON, an embedded Tomcat, validation glue and their transitive dependencies — at versions that are known to work together. See for yourself:
./mvnw dependency:treeNotice what is missing above: a <version>. It comes from the parent:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>The parent imports a bill of materials — a long list of managed versions. Choosing Boot 4.1.0 therefore chooses a Jackson version, a Hibernate version, a Tomcat version and a few hundred others, all tested together. This is the dependency-hell problem solved by someone else having already had the argument.
Not everything is managed. The pizza API pins springdoc by hand, and the comment explains why:
<properties>
<java.version>21</java.version>
<!-- Boot 4.1 does not manage springdoc, so the version is pinned here.
2.8.6 is verified working against Spring Framework 7.0.8. -->
<springdoc.version>2.8.6</springdoc.version>
</properties>If a dependency is not in the BOM, Maven tells you so in the least helpful way available:
'dependencies.dependency.version' for … is missing. That message means "not managed",
not "does not exist".
Auto-configuration
This is the part that feels like magic, and it is worth removing the magic early.
@EnableAutoConfiguration — inside @SpringBootApplication — makes Boot
read a list of candidate configuration classes shipped inside its own jars, then evaluate each
one's conditions against your application. A condition is usually "is this class on the classpath"
or "has the user already defined this bean".
Roughly, the JDBC one says: if a DataSource class is on the classpath, and a
connection pool is available, and the user has not defined their own DataSource
bean, then create one from the spring.datasource.* properties.
// The shape of a Boot auto-configuration, paraphrased.
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // <- backs off if you defined your own
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}@ConditionalOnMissingBean is the whole philosophy in one annotation. Auto-configuration
is a default, never an imposition: define your own bean of that type and Boot silently steps aside.
You never have to "turn off" Boot to take control of something — you just do it.
Seeing every decision it made
When something is configured and you cannot find where, do not guess. Ask:
./mvnw spring-boot:run -Dspring-boot.run.arguments=--debugThat prints the condition evaluation report: every auto-configuration class, split into the ones that matched and the ones that did not, each with the reason.
Positive matches:
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required class 'javax.sql.DataSource' (OnClassCondition)
Negative matches:
-----------------
RabbitAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class
'com.rabbitmq.client.Channel' (OnClassCondition)Two questions this answers immediately, both of which otherwise cost an afternoon: why is this bean here? and why is this bean NOT here? The second is the more common one, and the answer is almost always a missing dependency rather than anything you did wrong.
To switch a specific auto-configuration off:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration⚠️ Boot 4 split autoconfiguration into modules
This is the change most likely to bite you when following an older tutorial. In Boot 3, adding a library often brought its autoconfiguration along. In Boot 4 the autoconfiguration frequently lives in a separate module, and depending on the plain library gives you the API with no autoconfiguration at all — which fails silently.
The pizza API's pom.xml carries a scar from exactly this:
<!-- Spring Boot 4 modularized autoconfiguration. Depending on plain
liquibase-core (all that Boot 3 needed) gives you the library with NO
autoconfiguration: Liquibase silently never runs and the failure shows up
as a confusing Hibernate "Schema validation: missing table" error. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase</artifactId>
</dependency>Note how the failure presented: not "Liquibase is missing" but "table not found". A missing autoconfiguration never announces itself — it produces a symptom several layers away. When something that should be automatic is not happening, the condition report above is the fastest route to the answer.
The embedded server
Traditional Java web deployment meant building a WAR and handing it to a servlet container you installed and configured separately. Boot inverts that: the server is a library your application starts.
./mvnw package
java -jar target/demo-0.0.1-SNAPSHOT.jarThat jar is executable and self-contained — application, dependencies and Tomcat. It is why Spring
Boot and containers fit together so well: the Dockerfile is a base JRE image and one
COPY.
Configure it with properties rather than a server config file:
server.port=8085
server.servlet.context-path=/api
server.tomcat.threads.max=200The pizza API's port comment is worth stealing, because the failure mode is genuinely confusing:
# 8085, deliberately. On this machine 8080 is taken by another Java app and 8099 by
# the lovemesomecoding admin API. Note that a Boot app binds *:port on IPv6 while some
# other servers bind 127.0.0.1 on IPv4, so a clashing `curl localhost:PORT` can quietly
# hit the OTHER app and return a plausible-looking 404.
server.port=8085What to take from this
- A starter is a curated dependency set; the parent supplies the versions.
- Auto-configuration is conditional and always backs off when you define your own bean. It is a default, not a constraint.
--debugprints the condition evaluation report, which answers both "why is this here" and "why is this missing".- In Boot 4, autoconfiguration often lives in its own module. A missing one fails silently and far from the cause.
Next: from Spring to Spring Boot, and Boot 3 to 4 — what the XML turns into, and the renames that break a Boot 3 build.