spring-boot-starter-web ships embedded Tomcat. How would you switch the embedded server to Jetty (or Undertow) instead?
answer
- Exclude spring-boot-starter-tomcat
- Add spring-boot-starter-jetty / -undertow
- Container chosen by classpath @ConditionalOnClass
- Forget to exclude -> Tomcat still wins
- WebFlux default = Netty, not Tomcat
basics
~10 sExclude spring-boot-starter-tomcat from spring-boot-starter-web, then add spring-boot-starter-jetty (or spring-boot-starter-undertow). Auto-configuration detects Jetty on the classpath and starts it as the embedded server instead of Tomcat.
solid answer
~40 sspring-boot-starter-web transitively includes spring-boot-starter-tomcat, which provides the embedded servlet container. To swap it, you remove Tomcat from the transitive set and add the alternative container's starter. In Maven, add an `<exclusion>` for `spring-boot-starter-tomcat` inside `spring-boot-starter-web`, then declare `spring-boot-starter-jetty`. In Gradle, `exclude(module = "spring-boot-starter-tomcat")` on the web starter and add `spring-boot-starter-jetty`. Spring Boot's embedded-container auto-configuration (`ServletWebServerFactoryAutoConfiguration`) picks the container by what's on the classpath: with only Jetty classes present, it wires a `JettyServletWebServerFactory` instead of Tomcat's. Undertow is the same pattern with `spring-boot-starter-undertow`. This is the canonical example of why starters expose their transitive pieces as sub-starters — so a curated default (Tomcat) stays overridable.
code
kotlin · 9 lines// build.gradle.kts — replace embedded Tomcat with Jetty
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web") {
// remove the default embedded container
exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat")
}
// add the alternative; auto-config detects Jetty on the classpath
implementation("org.springframework.boot:spring-boot-starter-jetty")
}go deeper
Know Tomcat is the default and can be swapped by changing dependencies.
Perform the exclusion + add-alternative recipe and explain that classpath drives the choice.
Cite ServletWebServerFactoryAutoConfiguration's @ConditionalOnClass selection and the WebFlux/Netty distinction.
Standardize container choice across services, reason about footprint/throughput trade-offs, and manage WAR-vs-embedded deployment concerns.
## Why Tomcat is there in the first place `spring-boot-starter-web` is a curated bundle. One of its transitive dependencies is **`spring-boot-starter-tomcat`**, itself a starter that pulls the embedded Tomcat artifacts (`tomcat-embed-core`, `tomcat-embed-el`, `tomcat-embed-websocket`). Boot chose Tomcat as the **default** embedded servlet container. ## The mechanism that picks the container Spring Boot does **not** hardcode Tomcat. `ServletWebServerFactoryAutoConfiguration` has nested configurations guarded by `@ConditionalOnClass`: - If Tomcat classes are present -> `TomcatServletWebServerFactory` bean. - If Jetty classes are present (and Tomcat isn't) -> `JettyServletWebServerFactory`. - If Undertow classes are present -> `UndertowServletWebServerFactory`. So the container is chosen by **classpath contents**. Change the classpath, change the server. This is a concrete demonstration of the starter-plus-conditional-auto-config design. ## How to swap — Maven ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> ``` ## How to swap — Gradle (Kotlin DSL) ```kotlin implementation("org.springframework.boot:spring-boot-starter-web") { exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat") } implementation("org.springframework.boot:spring-boot-starter-jetty") ``` ## Undertow Same recipe, swap in `spring-boot-starter-undertow`. ## Gotchas - **Leaving Tomcat on the classpath**: if you add Jetty but forget to exclude Tomcat, both containers' classes are present. Boot's conditionals are ordered so Tomcat typically still wins — you must exclude it, not just add Jetty. - **WebFlux vs MVC**: `spring-boot-starter-webflux` uses **Netty** by default (`spring-boot-starter-reactor-netty`), not Tomcat. The exclusion targets differ. - **Only exclude the sub-starter, not individual jars**: excluding `spring-boot-starter-tomcat` cleanly removes all three embed jars; excluding one embed artifact leaves the others. - **Provided/WAR deployment**: if deploying a WAR to an external container, you instead mark the servlet starter `provided`/`compileOnly` rather than swapping — different concern. ## When to swap Rarely for greenfield (Tomcat is a fine default). Reasons: Jetty for lighter footprint / long-lived connection handling, Undertow for high-throughput non-blocking IO, or aligning with an org standard.
- What actually decides which embedded server starts at runtime?ServletWebServerFactoryAutoConfiguration uses @ConditionalOnClass to pick a factory (Tomcat/Jetty/Undertow) based on which container classes are on the classpath. So excluding Tomcat and adding Jetty changes the classpath, and Boot wires JettyServletWebServerFactory.
- Why isn't it enough to just add spring-boot-starter-jetty without the exclusion?Both Tomcat and Jetty classes would then be present. Boot's conditional ordering generally still selects Tomcat, so you must exclude spring-boot-starter-tomcat for Jetty to take over.
saying these in an interview costs you the question
- Thinking the embedded server is hardcoded and can't be changed
- Adding Jetty without excluding Tomcat and expecting Jetty to win
- Assuming spring-boot-starter-webflux also defaults to Tomcat (it uses Reactor Netty)