Spring Boot – What It Actually Does

June 13, 20264 min readUpdated 8/18/2026

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:tree

Notice 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=--debug

That 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.jar

That 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=200

The 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=8085

What 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.
  • --debug prints 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.