Spring Boot Essentials

The Embedded Server

18 min Lesson 5 of 13

The Embedded Server

One of the most visible differences between a traditional Java web application and a Spring Boot application is how you run it. In the traditional model you packaged your code as a WAR file and deployed it into a separately installed application server — Tomcat, JBoss, WebLogic. In Spring Boot the server ships inside your JAR. You run java -jar myapp.jar and Tomcat starts on its own. This lesson explains exactly how that works, why it matters, and how to configure and swap it.

What "Embedded" Means

An embedded server is an application server whose lifecycle is managed by your application code, not the other way around. Spring Boot pulls this off by including the server as an ordinary dependency. When your main() method calls SpringApplication.run(), the framework:

  1. Creates the Spring ApplicationContext.
  2. Detects the embedded-server dependency on the classpath.
  3. Programmatically instantiates and configures the server (e.g., org.apache.catalina.startup.Tomcat).
  4. Registers your DispatcherServlet with it.
  5. Starts the server and binds it to a port.

The entire bootstrap happens inside the JVM process. When you kill the process the server stops too — no separate shutdown script, no undeploy step.

The Three Built-in Server Choices

Spring Boot ships auto-configuration for three embedded servers. You choose one by controlling which starter is on your classpath:

  • Tomcat (default) — mature, ubiquitous, battle-tested. Pulled in automatically by spring-boot-starter-web.
  • Jetty — lightweight, historically strong at handling long-lived connections (WebSocket, HTTP/2 streaming).
  • Undertow — high-throughput, non-blocking I/O core, low memory footprint per connection. Popular for reactive workloads even in the servlet stack.

To switch from Tomcat to Undertow you exclude the Tomcat starter and add the Undertow starter in your pom.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-undertow</artifactId> </dependency>

No code changes are needed — Spring Boot detects the new server on the classpath and wires it up identically. This is auto-configuration doing its job.

Configuring the Server via application.properties

The most common server settings live under the server.* namespace in application.properties (or application.yml):

# Port (default 8080; use 0 to let the OS pick a random free port) server.port=8080 # Context path — all endpoints will be under /api server.servlet.context-path=/api # Connection and thread limits (Tomcat-specific) server.tomcat.threads.max=200 server.tomcat.threads.min-spare=10 server.tomcat.accept-count=100 server.tomcat.max-connections=8192 # Request size limits server.tomcat.max-http-form-post-size=2MB server.tomcat.max-swallow-size=2MB # HTTPS — enable SSL on port 8443 server.port=8443 server.ssl.key-store=classpath:keystore.p12 server.ssl.key-store-password=changeit server.ssl.key-store-type=PKCS12
Random port trick: Setting server.port=0 tells the OS to assign any available port. This is extremely useful in integration tests — each test run gets a unique port, eliminating port-conflict flakiness. Inject the actual chosen port in a test with @LocalServerPort.

Programmatic Server Customisation

Properties cover 80% of use cases. For the rest — custom connectors, custom error pages, Gzip compression tuned beyond what properties expose — you implement a WebServerFactoryCustomizer. The generic version works across all three servers; server-specific sub-interfaces give you access to vendor APIs:

import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.stereotype.Component; @Component public class TomcatTuning implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> { @Override public void customize(TomcatServletWebServerFactory factory) { // Add a second connector on port 8009 (AJP — rarely needed, shown for demo) factory.addConnectorCustomizers(connector -> connector.setProperty("relaxedQueryChars", "|{}[]") ); // Enable Gzip compression factory.addConnectorCustomizers(connector -> { connector.setProperty("compression", "on"); connector.setProperty("compressibleMimeType", "text/html,application/json,application/javascript"); connector.setProperty("compressionMinSize", "1024"); }); } }
Prefer application.properties for standard settings. Reserve WebServerFactoryCustomizer for things that genuinely have no property equivalent. Customizers that use the server-specific interface (e.g. TomcatServletWebServerFactory) break if you later swap servers; the generic ConfigurableServletWebServerFactory is portable.

Graceful Shutdown

By default, when Spring Boot receives a shutdown signal (SIGTERM) it stops the server immediately — in-flight requests are cut off. Since Spring Boot 2.3 you can enable graceful shutdown, which allows active requests to complete before the process exits:

# application.properties spring.lifecycle.timeout-per-shutdown-phase=30s server.shutdown=graceful

With server.shutdown=graceful the server stops accepting new connections the moment the signal arrives but continues processing requests already in progress, up to the timeout-per-shutdown-phase limit. This is essential for zero-downtime rolling deployments in Kubernetes or behind a load balancer.

Graceful shutdown does not help if your thread pool is saturated. If all Tomcat threads are occupied by slow requests when SIGTERM arrives, those requests will block shutdown until the timeout expires and then be killed anyway. Size your thread pool and timeouts to reflect realistic request durations.

The Executable JAR and How It Works

When you run mvn package (or ./gradlew bootJar), the Spring Boot build plugin creates a fat JAR — a single archive that contains your compiled classes, all dependency JARs (including the server JAR), and a custom launcher. The launcher understands nested JARs (the standard Java class loader does not), loads them all, and then invokes your @SpringBootApplication class's main() method.

# Build the fat JAR mvn clean package -DskipTests # Run it — the embedded Tomcat starts on port 8080 java -jar target/myapp-0.0.1-SNAPSHOT.jar # Override the port at runtime without changing properties java -jar target/myapp-0.0.1-SNAPSHOT.jar --server.port=9090

The exploded-JAR layout inside the archive is:

myapp.jar ├── BOOT-INF/ │ ├── classes/ <-- your compiled code │ └── lib/ <-- all dependency JARs (including tomcat-embed-core) ├── META-INF/ │ └── MANIFEST.MF <-- Main-Class: org.springframework.boot.loader.JarLauncher └── org/springframework/boot/loader/ <-- the launcher classes

WAR Deployment: When You Still Need It

Some organisations run a shared Tomcat or JBoss instance managed by operations. In that case you produce a WAR instead of a JAR. Change the packaging in pom.xml, extend SpringBootServletInitializer, and mark the embedded server as provided scope:

// src/main/java/com/example/MyApp.java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @SpringBootApplication public class MyApp extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(MyApp.class); } public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } }
Both modes from one codebase: Because main() is still present, the same project runs as an executable JAR in development and can be deployed as a WAR in production. The provided scope on the Tomcat starter means it is excluded from the WAR (the container provides it) but kept on the classpath when running locally via main().

Summary

Spring Boot embeds Tomcat (or Jetty or Undertow) as a library inside your JAR, making java -jar the complete deployment unit. You configure the server through the server.* property namespace for common settings and through WebServerFactoryCustomizer for advanced tuning. Graceful shutdown with a configured timeout is a must for any production workload that expects zero-downtime restarts. When organisational constraints require an external container, switching to WAR packaging requires only three small changes and keeps the same source tree.