skip to content

spring-boot-starter-web ships embedded Tomcat. How would you switch the embedded server to Jetty (or Undertow) instead?

level: middleimportance: should knowfreq 45%

answer

  1. Exclude spring-boot-starter-tomcat
  2. Add spring-boot-starter-jetty / -undertow
  3. Container chosen by classpath @ConditionalOnClass
  4. Forget to exclude -> Tomcat still wins
  5. WebFlux default = Netty, not Tomcat

basics

~10 s

Exclude 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 s

spring-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
kotlin
// 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

for a junior

Know Tomcat is the default and can be swapped by changing dependencies.

for a middle

Perform the exclusion + add-alternative recipe and explain that classpath drives the choice.

for a senior

Cite ServletWebServerFactoryAutoConfiguration's @ConditionalOnClass selection and the WebFlux/Netty distinction.

for a principal

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)

context