Command Palette

Search for a command to run...

[Spring Boot Basics] Spring Framework vs Spring Boot: What Auto-Configuration, Starters and the Embedded Server Actually Do

Spring Framework and Spring Boot are not competitors, and Boot is not a newer Spring. Boot is a layer of defaults that sits on top of the Framework, decides what a typical application probably wants, and configures it for you. Take the layer away and every Spring concept underneath is unchanged — you just write all the configuration yourself.

That makes "what is the difference" a question with a concrete, measurable answer: it is the set of things Boot does so you do not have to. This article measures them. Every version number, dependency tree and log line below came out of a real build.

On the left a stack of things you configure by hand, on the right a single starter line that replaces them

Everything here was produced on OpenJDK 21.0.6 with Spring Boot 4.1.1 and Gradle 9.7.1, using a project generated by Spring Initializr and built with the Gradle wrapper it ships.

Spring Framework is the container, Spring Boot is the opinions

Spring Framework is the runtime: it constructs your objects, injects their collaborators, proxies them for transactions and security, and provides the web stack. It is old, stable and deliberately unopinionated — it will do whatever you configure it to do, and historically you configured all of it.

Spring Boot adds exactly four things on top, and nothing else conceptually new:

Boot contributesWhat it removes
Startershunting down the fifteen artifacts a feature needs
Dependency management (a BOM)picking versions that work together
Auto-configurationwriting the @Configuration classes a typical app would write anyway
Embedded server and launcherinstalling a servlet container and deploying a WAR into it

A Boot release is pinned to one Framework release, and you do not choose them independently. Boot 4.1.1 is Framework 7.0.9, and the spring-boot-dependencies BOM says so directly:

XML
<spring-framework.version>7.0.9</spring-framework.version>

The resolved dependency tree agrees. Every org.springframework artifact on the classpath of a Boot 4.1.1 project arrives at 7.0.9 without anyone asking for it:

Text
\--- org.springframework.boot:spring-boot-starter-webmvc -> 4.1.1
     +--- org.springframework.boot:spring-boot-starter:4.1.1
     |    +--- org.springframework.boot:spring-boot-autoconfigure:4.1.1
     |    |    \--- org.springframework.boot:spring-boot:4.1.1
     |    |         +--- org.springframework:spring-core:7.0.9
     |    |         \--- org.springframework:spring-context:7.0.9

Two practical consequences of the 4.x line. Boot 4 is compiled for Java 17 — the class files in spring-boot-4.1.1.jar carry major version 61 — so 17 is the floor and 21 or 25 is what you would actually pick. And Boot 3.5 left open-source support on 2026-06-30, which is why a new project started today begins at 4.x rather than 3.x.

What a plain Spring Framework web application made you write

Before Boot, "a Spring web application" meant assembling the pieces yourself. The shape below is illustrative rather than something worth running today, but every item in it was real work.

First, a bootstrap class telling the servlet container that Spring exists and which URLs it owns — or the web.xml equivalent:

WebInit.java
public class WebInit implements WebApplicationInitializer {
 
    @Override
    public void onStartup(ServletContext ctx) {
        var ac = new AnnotationConfigWebApplicationContext();
        ac.register(WebConfig.class);
 
        var reg = ctx.addServlet("dispatcher", new DispatcherServlet(ac));
        reg.setLoadOnStartup(1);
        reg.addMapping("/");
    }
}

Then a configuration class enabling the MVC infrastructure and declaring how responses get serialised:

WebConfig.java
@Configuration
@EnableWebMvc
@ComponentScan("com.example.demo")
public class WebConfig implements WebMvcConfigurer {
 
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}

Then a build file naming every artifact with its own version, a WAR packaging step, and a Tomcat installed on the machine that would eventually host the WAR.

The measurable part of this is the dependency graph. Asking only for spring-webmvc — with the version typed in by hand, because nothing is managing it — resolves to eleven jars and 8.3 MiB:

Text
\--- org.springframework:spring-webmvc:7.0.9
     +--- org.springframework:spring-aop:7.0.9
     +--- org.springframework:spring-beans:7.0.9
     +--- org.springframework:spring-context:7.0.9
     |    \--- io.micrometer:micrometer-observation:1.16.7
     +--- org.springframework:spring-expression:7.0.9
     \--- org.springframework:spring-web:7.0.9

That is the honest baseline: the MVC stack and nothing else. No servlet container, no JSON library, no logging backend, no configuration binding, no way to start the thing. All of those were separate decisions with separate version numbers.

What Spring Boot puts in its place

The same application, on Boot, is one class:

src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class DemoApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Thirteen lines, of which four are imports and package declaration. There is no initializer, no @EnableWebMvc, no DispatcherServlet registration, no message converter list, and no servlet container to install. The build file that goes with it is twenty-three non-blank lines, and the dependency block is four of them:

build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

The seven jobs a web application has to do, with the plain Spring Framework answer and the Spring Boot answer on the same row

Read row by row, nothing on the Boot side is a new Spring concept. DispatcherServletAutoConfiguration registers the same DispatcherServlet that WebInit registered above. The difference is who writes the registration.

⚠️ On Boot 4 the web starter is spring-boot-starter-webmvc, not spring-boot-starter-web. The old coordinate still resolves — to the identical 39-jar classpath, in fact — but its POM on Maven Central now describes itself as "Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container (deprecated in favor of spring-boot-starter-webmvc)". On the test side, Initializr now generates the narrower spring-boot-starter-webmvc-test rather than the general spring-boot-starter-test, which still exists and is not deprecated. Practically every tutorial you will find still says -web, because it was the spelling for a decade.

What is a Spring Boot starter?

A starter is a POM with no code in it. It compiles to no classes, ships no annotations and contributes no behaviour of its own. All it does is name a curated set of other artifacts, so that adding one coordinate to your build adds the whole coherent group.

spring-boot-starter-webmvc declares five direct dependencies. Resolving them transitively produces 39 jars and 18.8 MiB on the runtime classpath:

One starter line expanding into its five direct dependencies and the transitive tree beneath them

The real report, trimmed to the first level:

Text
\--- org.springframework.boot:spring-boot-starter-webmvc -> 4.1.1
     +--- org.springframework.boot:spring-boot-starter:4.1.1
     +--- org.springframework.boot:spring-boot-starter-jackson:4.1.1
     +--- org.springframework.boot:spring-boot-starter-tomcat:4.1.1
     +--- org.springframework.boot:spring-boot-http-converter:4.1.1
     \--- org.springframework.boot:spring-boot-webmvc:4.1.1

Three of those five are themselves starters. spring-boot-starter is the base every other starter depends on — the core container plus logging plus YAML parsing. spring-boot-starter-tomcat is the embedded servlet container. spring-boot-starter-jackson is JSON. That composition is why one line gave you a running HTTP server that speaks JSON.

Boot 4.1.1 publishes 170 starters. These are the ones you will meet first, with the descriptions from their own POMs:

StarterWhat it is for
spring-boot-startercore starter, including auto-configuration support, logging and YAML
spring-boot-starter-webmvcSpring MVC and Tomcat
spring-boot-starter-webfluxWebFlux and Reactor Netty, for the reactive stack
spring-boot-starter-data-jpaSpring Data JPA with Hibernate
spring-boot-starter-jdbcJDBC with the HikariCP connection pool
spring-boot-starter-securitySpring Security
spring-boot-starter-validationJava Bean Validation with Hibernate Validator
spring-boot-starter-actuatorproduction-ready endpoints for monitoring and management
spring-boot-starter-thymeleafserver-rendered HTML templates
spring-boot-starter-restclientthe blocking HTTP clients — RestClient, RestTemplate, HTTP service clients
spring-boot-starter-cacheSpring's caching abstraction
spring-boot-starter-testJUnit Jupiter, Hamcrest and Mockito, wired for Spring

There is a naming convention worth knowing, because it tells you who maintains a dependency at a glance. spring-boot-starter-* is reserved for starters the Spring team publishes. A third party naming a starter puts its own name first: mybatis-spring-boot-starter, mybatis-plus-spring-boot3-starter. If you ever publish one, follow that rule — a coordinate beginning spring-boot-starter- claims to be official.

Where do the version numbers come from?

Look again at the dependency block above: not one version number. That is not the Boot plugin being clever at runtime, it is a bill of materials — a POM whose only job is to declare "if you use this artifact, use this version". spring-boot-dependencies:4.1.1 manages 652 dependencies through 195 version properties.

In Gradle it is applied by the io.spring.dependency-management plugin, which Initializr puts in the build file next to the Boot plugin:

build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '4.1.1'
    id 'io.spring.dependency-management' version '1.1.7'
}

Those two plugins do different jobs, and it is worth not confusing them. The Boot plugin builds the executable jar and wires up bootRun. The dependency-management plugin imports the BOM so version-less coordinates resolve. Delete the second line and the build stops immediately:

Text
* What went wrong:
Execution failed for task ':compileJava'.
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not find org.springframework.boot:spring-boot-starter-webmvc:.
     Required by:
         root project 'demo'

The trailing colon with nothing after it is the empty version — Gradle asked for a version and the BOM was not there to supply one.

What the BOM buys you is alignment, not just convenience. Half the jars on that 39-jar classpath have versions you never typed anywhere:

ArtifactVersionWhere it came from
tomcat-embed-core11.0.24tomcat.version in the BOM
logback-classic1.5.38logback.version in the BOM
jackson-databind3.1.5jackson-bom.version in the BOM
snakeyaml2.6snakeyaml.version in the BOM
slf4j-api2.0.18slf4j.version in the BOM

Alignment shows up most clearly where two libraries disagree. spring-webmvc:7.0.9 declares a dependency on micrometer-observation:1.16.7. On the plain build, that is exactly what you get. Under Boot, the BOM overrides it upward and Gradle prints the substitution:

Text
io.micrometer:micrometer-observation:1.16.7 -> 1.17.1

Nobody asked for 1.17.1. The BOM decided that is the Micrometer that the whole of Boot 4.1.1 was tested against, and every module on the classpath now agrees on it. Doing that by hand across 652 artifacts is the work Boot is actually saving you.

What auto-configuration actually does

The idea is small enough to state in one sentence: at startup, Boot looks at what is on the classpath and in your configuration, and creates the beans a typical application would have created for itself — but only when you have not created them already.

The back-off is the part that makes it usable. Auto-configuration classes guard their beans with @ConditionalOnMissingBean, so the moment you declare a bean of the same type, Boot's version quietly steps aside and yours wins. There is nothing to disable and no fight to win; declaring the bean is the override.

Running the application with --debug prints a CONDITIONS EVALUATION REPORT showing every decision. On this bare project it reports 52 positive and 39 negative matches, including the one that registers the servlet the old WebInit class had to register manually:

Text
DispatcherServletAutoConfiguration matched:
   - @ConditionalOnClass found required class
     'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
   - found 'session' scope (OnWebApplicationCondition)

Reading that report properly — and the @Conditional* family behind it — is a chapter of its own, and this series gets to it later. For now the useful mental model is: classpath in, beans out, and anything you declare yourself takes precedence.

Embedded server or WAR: the two deployment shapes

Classic Spring deployment produced a WAR that you copied into a servlet container someone else installed and operated. Boot inverts the containment: the build produces one jar that contains the server.

A WAR nested inside an installed Tomcat on the left, and a fat jar containing tomcat-embed-core on the right

The jar is genuinely self-contained. Its manifest points at Boot's own launcher rather than your class, and the nested layout is standard:

Text
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Version: 4.1.1
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

BOOT-INF/lib/ holds 35 jars, tomcat-embed-core-11.0.24.jar among them, and the whole artifact is 19.0 MiB. Running it needs a JVM and nothing else:

Bash
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar
Text
INFO 12815 --- [demo] [main] o.s.boot.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
INFO 12815 --- [demo] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.24]
INFO 12815 --- [demo] [main] o.s.boot.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
INFO 12815 --- [demo] [main] com.example.demo.DemoApplication : Started DemoApplication in 0.561 seconds (process running for 0.744)

That startup time is indicative only — it is a warm cache on a developer machine, not a benchmark.

What actually changes between the two shapes:

WAR in an external containerExecutable jar
Server versionchosen and patched by whoever runs the serverpinned in your build file, travels with the artifact
Server configserver.xml, container-wideapplication.properties, per application
Start commandthe container's own scriptsjava -jar app.jar
Portsthe container's, shared across deployed appsyours, one process per application
Testsneed a container, or a mocked onethe real server starts in-process on a random port
Failure blast radiusone bad app can take down its neighboursone process, one application

Swapping the embedded server

Tomcat is the default because spring-boot-starter-webmvc pulls spring-boot-starter-tomcat. Exclude that and add another:

build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    implementation('org.springframework.boot:spring-boot-starter-webmvc') { 
        exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
    } 
    implementation 'org.springframework.boot:spring-boot-starter-jetty'
}

No code changes, no configuration changes. The same application starts on Jetty:

Text
INFO 14295 --- [demo] [main] org.eclipse.jetty.server.Server : Started oejs.Server@799ed4e8{STARTING}[12.1.12,sto=0] @44ms
INFO 14295 --- [demo] [main] o.s.boot.jetty.JettyWebServer   : Jetty started on port 8080 (http/1.1) with context path '/'

One correction to almost every article written about this: Boot 4 has no Undertow starter. spring-boot-starter-undertow exists up to 3.5.x and is gone from 4.x — it is not in the spring-boot-dependencies:4.1.1 BOM, and the coordinate returns a 404 on Maven Central. On Boot 4 the embedded servlet containers are Tomcat and Jetty, with Reactor Netty for the reactive stack.

If you still have to ship a WAR

Boot supports it. Ask Initializr for packaging=war and the generated project adds the war plugin, moves the container to providedRuntime so it is not packed into the archive, and includes a small initializer class:

build.gradle
plugins {
    id 'java'
    id 'war'
    id 'org.springframework.boot' version '4.1.1'
    id 'io.spring.dependency-management' version '1.1.7'
}
 
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat-runtime'
}
ServletInitializer.java
public class ServletInitializer extends SpringBootServletInitializer {
 
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(WarpApplication.class);
    }
}

That class is the modern descendant of the WebApplicationInitializer from the first section — the external container finds it, and it hands control to Boot.

The Spring ecosystem you will actually meet

"Spring" is an umbrella over dozens of projects. These are the ones a working developer runs into, with the versions the Boot 4.1.1 BOM manages:

ProjectWhat it is forVersion under Boot 4.1.1
Spring Frameworkthe container, DI, AOP, transactions, the web stack7.0.9
Spring Bootstarters, auto-configuration, embedded server, the BOM4.1.1
Spring Datarepository abstractions over JPA, MongoDB, Redis, JDBC and moreBOM 2026.0.1
Spring Securityauthentication, authorisation, OAuth2, method security7.1.1
Spring Batchchunk-oriented batch jobs with restart and retry6.0.5
Spring Integrationenterprise integration patterns as message channels7.1.1
Spring for Apache KafkaKafka producers, consumers and listener containers4.1.1
Spring AMQPthe same for RabbitMQ4.1.1
Spring GraphQLa GraphQL endpoint on the Spring web stack2.0.5
Spring SessionHTTP session storage in Redis or JDBC instead of the server4.1.1
Spring Web Servicescontract-first SOAP, where you still need it5.0.2
Spring Cloudconfig server, service discovery, gateway, resilienceits own BOM
Spring AImodel clients, embeddings, vector stores, RAG plumbingits own BOM
Spring Modulithenforcing module boundaries inside one deployableits own BOM

The last three are worth calling out: Spring Cloud, Spring AI and Spring Modulith are not in the Boot BOM. Each publishes its own, and you import it alongside Boot's and match its release train to your Boot version. Everything above them comes along for free with the Boot version you already chose.

When Spring Boot is not the answer

Boot is a default, not a law, and its opinions cost something.

  • A library other people embed. If your artifact is a dependency rather than a deployable, Boot's fat jar and auto-configuration are the wrong shape. Publish a plain jar, and use plain Spring Framework or no framework at all.
  • A one-shot script. Thirty-nine jars and a 19 MiB artifact to parse a CSV is not a trade worth making.
  • A shared application-server estate. If operations runs a Tomcat farm and mandates WARs into it, you can still use Boot, but you lose the deployment simplification that is most of the point.
  • A working Spring Framework application. Migrating an existing, stable XML-configured application to Boot buys you nothing on its own. Migrate when you are changing it anyway.
  • Hard startup or memory ceilings. Classpath scanning and reflection are not free. Boot has AOT processing and GraalVM native images for this, but if you are fighting for tens of milliseconds a smaller framework may simply fit better.

For a server-side application you own and deploy yourself — which is most of what gets written — the trade is overwhelmingly in Boot's favour, and that is why the rest of this series assumes it.

FAQ

Is Spring Boot a replacement for Spring Framework?

No. Boot depends on the Framework and cannot exist without it. Boot 4.1.1 pulls in Framework 7.0.9, and every bean, every annotation and the entire container are Framework code. Boot only decides what gets configured.

Can I use Spring Framework without Spring Boot?

Yes, and it is still fully supported. You declare spring-webmvc or spring-context directly, choose every version yourself, and write the configuration classes. The tree above shows the whole cost: eleven jars for the MVC stack, with the server, JSON and logging left for you to add.

Why is it spring-boot-starter-webmvc and not spring-boot-starter-web?

Boot 4 split its modules more finely and renamed the web starter to say which web stack it means, since spring-boot-starter-webflux is the other option. spring-boot-starter-web still resolves on 4.1.1 and produces exactly the same 39 jars — the only difference in the resolved classpath is the name of the starter jar itself — but its own POM marks it deprecated in favour of spring-boot-starter-webmvc.

Does auto-configuration mean I lose control?

No — it is explicitly designed to lose the argument. Auto-configured beans are guarded by @ConditionalOnMissingBean, so declaring your own bean of that type removes Boot's. You can also exclude an auto-configuration class outright, and --debug prints the report of what matched and what did not.

Is the embedded server production-ready?

Yes. It is the same Apache Tomcat 11.0.24 you would install standalone, started in-process instead of by a script. The practical differences are operational — one process per application, configuration in application.properties, and the server version pinned by your build rather than by whoever patches the server.

How do I know which version of a library Spring Boot will give me?

Read spring-boot-dependencies for your Boot version, or ask the build. ./gradlew dependencies --configuration runtimeClasspath prints the resolved tree with every substitution marked by an arrow, which is how the micrometer-observation:1.16.7 -> 1.17.1 upgrade above was found.

Conclusion

The difference between Spring Framework and Spring Boot is not a feature list, it is a division of labour. The Framework runs your beans; Boot decides which beans a normal application needs, pins every version that goes with them, and packages the result with a server inside it. One starter line, four dependencies, 39 jars, and a DispatcherServlet you never registered.

Next article: setting up the environment — installing the JDK and an IDE, generating a project with Spring Initializr, and running your first Hello World application.

Related Posts

[Spring Boot Basics] Validation in Spring Boot: Bean Validation Annotations, @Valid and Custom Validators

Bean Validation in Spring Boot 4.1.1 with Hibernate Validator 9.1.3, checked against real runs: spring-boot-starter-validation, @NotNull vs @NotEmpty vs @NotBlank, @Size, @DecimalMin, @Digits, @Email and @Pattern on request DTO records, @Valid on @RequestBody and the default 400, nested objects and lists, @PathVariable and @RequestParam validation and the @Validated 500 trap, validation groups, ValidationMessages.properties and Accept-Language, custom ConstraintValidator and cross-field constraints, and validation in the service layer.

[Spring Boot Basics] How Spring Boot Auto-Configuration Works: Conditions, Back-Off and the --debug Report

The mechanism behind Spring Boot 4.1.1 auto-configuration, opened up and measured: @EnableAutoConfiguration and AutoConfigurationImportSelector, the META-INF/spring/…AutoConfiguration.imports files that Boot 4 spreads across small modules, the @ConditionalOnClass / @ConditionalOnMissingBean family with a custom Condition of your own, a back-off demonstration with real before-and-after numbers, and how to read the --debug CONDITIONS EVALUATION REPORT.

[Spring Boot Basics] Setting Up Spring Boot: JDK, IDE, Spring Initializr and Your First Application

Install JDK 21 on macOS, Windows and Linux, fix a JAVA_HOME pointing at the wrong JDK, compare IntelliJ IDEA with VS Code, generate a Spring Boot 4.1.1 project from Spring Initializr or one curl command, run it with the Gradle wrapper, read the startup log line by line, write a @RestController that returns JSON, change server.port, and fix the five errors every beginner hits.

[Spring Boot Basics] @ConfigurationProperties in Spring Boot: Type-Safe Configuration with Validation

Type-safe configuration in Spring Boot 4.1.1 with @ConfigurationProperties, checked against real runs: binding to records without @ConstructorBinding, JavaBean binding and @DefaultValue, the three ways to register a properties class, nested objects, lists, maps, enums, Duration and DataSize conversion, relaxed binding and environment variable names, @Validated fail-fast startup errors, the configuration processor metadata, and a side-by-side comparison with @Value.