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.
![]()
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 contributes | What it removes |
|---|---|
| Starters | hunting down the fifteen artifacts a feature needs |
| Dependency management (a BOM) | picking versions that work together |
| Auto-configuration | writing the @Configuration classes a typical app would write anyway |
| Embedded server and launcher | installing 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:
<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:
\--- 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.9Two 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:
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:
@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:
\--- 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.9That 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:
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:
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'
}
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, notspring-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 narrowerspring-boot-starter-webmvc-testrather than the generalspring-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:

The real report, trimmed to the first level:
\--- 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.1Three 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:
| Starter | What it is for |
|---|---|
spring-boot-starter | core starter, including auto-configuration support, logging and YAML |
spring-boot-starter-webmvc | Spring MVC and Tomcat |
spring-boot-starter-webflux | WebFlux and Reactor Netty, for the reactive stack |
spring-boot-starter-data-jpa | Spring Data JPA with Hibernate |
spring-boot-starter-jdbc | JDBC with the HikariCP connection pool |
spring-boot-starter-security | Spring Security |
spring-boot-starter-validation | Java Bean Validation with Hibernate Validator |
spring-boot-starter-actuator | production-ready endpoints for monitoring and management |
spring-boot-starter-thymeleaf | server-rendered HTML templates |
spring-boot-starter-restclient | the blocking HTTP clients — RestClient, RestTemplate, HTTP service clients |
spring-boot-starter-cache | Spring's caching abstraction |
spring-boot-starter-test | JUnit 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:
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:
* 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:
| Artifact | Version | Where it came from |
|---|---|---|
tomcat-embed-core | 11.0.24 | tomcat.version in the BOM |
logback-classic | 1.5.38 | logback.version in the BOM |
jackson-databind | 3.1.5 | jackson-bom.version in the BOM |
snakeyaml | 2.6 | snakeyaml.version in the BOM |
slf4j-api | 2.0.18 | slf4j.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:
io.micrometer:micrometer-observation:1.16.7 -> 1.17.1Nobody 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:
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.

The jar is genuinely self-contained. Its manifest points at Boot's own launcher rather than your class, and the nested layout is standard:
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:
java -jar build/libs/demo-0.0.1-SNAPSHOT.jarINFO 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 container | Executable jar | |
|---|---|---|
| Server version | chosen and patched by whoever runs the server | pinned in your build file, travels with the artifact |
| Server config | server.xml, container-wide | application.properties, per application |
| Start command | the container's own scripts | java -jar app.jar |
| Ports | the container's, shared across deployed apps | yours, one process per application |
| Tests | need a container, or a mocked one | the real server starts in-process on a random port |
| Failure blast radius | one bad app can take down its neighbours | one 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:
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:
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:
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'
}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:
| Project | What it is for | Version under Boot 4.1.1 |
|---|---|---|
| Spring Framework | the container, DI, AOP, transactions, the web stack | 7.0.9 |
| Spring Boot | starters, auto-configuration, embedded server, the BOM | 4.1.1 |
| Spring Data | repository abstractions over JPA, MongoDB, Redis, JDBC and more | BOM 2026.0.1 |
| Spring Security | authentication, authorisation, OAuth2, method security | 7.1.1 |
| Spring Batch | chunk-oriented batch jobs with restart and retry | 6.0.5 |
| Spring Integration | enterprise integration patterns as message channels | 7.1.1 |
| Spring for Apache Kafka | Kafka producers, consumers and listener containers | 4.1.1 |
| Spring AMQP | the same for RabbitMQ | 4.1.1 |
| Spring GraphQL | a GraphQL endpoint on the Spring web stack | 2.0.5 |
| Spring Session | HTTP session storage in Redis or JDBC instead of the server | 4.1.1 |
| Spring Web Services | contract-first SOAP, where you still need it | 5.0.2 |
| Spring Cloud | config server, service discovery, gateway, resilience | its own BOM |
| Spring AI | model clients, embeddings, vector stores, RAG plumbing | its own BOM |
| Spring Modulith | enforcing module boundaries inside one deployable | its 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.