skip to content

How do you switch a Spring Boot servlet app from Tomcat to Jetty or Undertow?

level: middleimportance: must knowfreq 60%

answer

  1. exclude starter-tomcat, add starter-jetty/undertow
  2. classpath-driven, not a flag
  3. one container starter only
  4. Tomcat branch wins if both present
  5. server.<container>.* tuning keys differ

basics

~10 s

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

The 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
xml
<!-- 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

for a junior

Know you exclude Tomcat and add the Jetty/Undertow starter.

for a middle

Explain it's classpath-driven, the exclusion is mandatory, and code is unchanged.

for a senior

Reference the conditional branches in ServletWebServerFactoryAutoConfiguration and per-container tuning keys.

for a principal

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

context