How do you switch a Spring Boot servlet app from Tomcat to Jetty or Undertow?
answer
- exclude starter-tomcat, add starter-jetty/undertow
- classpath-driven, not a flag
- one container starter only
- Tomcat branch wins if both present
- server.<container>.* tuning keys differ
basics
~10 sExclude spring-boot-starter-tomcat from spring-boot-starter-web, then add spring-boot-starter-jetty (or spring-boot-starter-undertow). Boot's auto-configuration detects the new server on the classpath and uses it instead.
solid answer
~30 sThe selection is classpath-driven, so you change the classpath. Exclude the Tomcat starter from spring-boot-starter-web and add the alternative starter: spring-boot-starter-jetty or spring-boot-starter-undertow. Once Tomcat's classes are gone and Jetty's/Undertow's are present, ServletWebServerFactoryAutoConfiguration's conditional branches (EmbeddedJetty / EmbeddedUndertow) fire and build a JettyServletWebServerFactory or UndertowServletWebServerFactory instead of the Tomcat one. Only one servlet-container starter should be on the classpath — leaving two makes the auto-configuration ambiguous. The exclusion matters: just adding Jetty without removing Tomcat leaves both, and Tomcat typically still wins. Application code and most server.* properties are unchanged, though a few container-specific tuning properties differ.
code
xml · 16 lines<!-- Swap Tomcat for Undertow -->
<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-undertow</artifactId>
</dependency>
<!-- For Jetty, use spring-boot-starter-jetty instead. -->go deeper
Know you exclude Tomcat and add the Jetty/Undertow starter.
Explain it's classpath-driven, the exclusion is mandatory, and code is unchanged.
Reference the conditional branches in ServletWebServerFactoryAutoConfiguration and per-container tuning keys.
Weigh why/when to swap (footprint, throughput, ops standardization) and note it doesn't govern WebFlux.
Spring Boot chooses the embedded servlet container by **what's on the classpath**, not by configuration flags. So swapping servers = swapping dependencies. **The recipe (Maven):** 1. On `spring-boot-starter-web`, add an `<exclusion>` for `spring-boot-starter-tomcat`. 2. Add `spring-boot-starter-jetty` **or** `spring-boot-starter-undertow`. **Why the exclusion is required:** `spring-boot-starter-web` depends transitively on `spring-boot-starter-tomcat`. If you only *add* Jetty without *removing* Tomcat, both containers' classes sit on the classpath. `ServletWebServerFactoryAutoConfiguration` has ordered conditional branches (`EmbeddedTomcat`, `EmbeddedJetty`, `EmbeddedUndertow`), each `@ConditionalOnClass` for its container and `@ConditionalOnMissingBean(ServletWebServerFactory.class)`. Tomcat's branch is declared first, so with both present Tomcat generally wins — the opposite of what you intended. Remove Tomcat and the Jetty (or Undertow) branch is the one whose classes match, so it builds `JettyServletWebServerFactory` / `UndertowServletWebServerFactory`. **Gradle equivalent:** `configurations { implementation { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' } }` then add the new starter — or use `modules { module('...tomcat') { ... } }`. **What stays the same:** your `@Controller`/`@RestController` code, `DispatcherServlet`, and generic properties like `server.port`, `server.servlet.context-path`. All three are servlet containers implementing the same Jakarta Servlet API, so Spring MVC doesn't care which one runs. **What differs:** container-specific tuning. Thread-pool and connection properties live under container-scoped keys — e.g. `server.tomcat.threads.max`, `server.jetty.threads.max`, `server.undertow.threads.io` / `server.undertow.threads.worker`. Some advanced features (access logs, SSL tuning, HTTP/2 nuances) have per-container behavior. **Why swap at all:** Undertow (from JBoss/WildFly) is lightweight, non-blocking at its core, and often has a smaller memory footprint and good throughput; Jetty is popular for embeddability and WebSocket-heavy or long-lived-connection workloads; some shops standardize on one for ops/security reasons. In practice Tomcat is fine for most apps, so swap only for a measured need. **Gotcha:** none of this applies to WebFlux — that stack uses Netty by default and swapping servlet starters doesn't change a reactive app the same way.
- What happens if you add Jetty but forget to exclude Tomcat?Both containers are on the classpath. ServletWebServerFactoryAutoConfiguration evaluates Tomcat's branch first, so Tomcat typically still starts — your intended Jetty swap silently doesn't take effect.
- Do you have to change controller code when switching containers?No. All three (Tomcat/Jetty/Undertow) implement the Jakarta Servlet API, so Spring MVC and your controllers are unchanged. Only container-specific tuning properties (server.tomcat.* vs server.jetty.* vs server.undertow.*) differ.
saying these in an interview costs you the question
- Thinking there's a property like server.type=jetty to switch — there isn't; it's classpath-driven
- Adding the new starter without excluding Tomcat
- Keeping two container starters and expecting the second to win
- Assuming server.tomcat.* keys work for Jetty/Undertow