What Spring Boot Actually Does After java -jar (Startup to Graceful Shutdown)
▶ Watch on YouTube & subscribe to The Stack Underflow
You type java -jar app.jar, and a few seconds later the log says Started. Kubernetes starts sending traffic, and one day it sends SIGTERM. The video answers what happens in between, and in what order, with one hierarchical state machine (a Harel statechart). This page walks that chart state by state.
The one-line version: Java calls
main()first; beans are created insiderefresh(); Tomcat is created inonRefresh()but binds its port infinishRefresh(); Started is not Ready; on SIGTERM, readiness flips toREFUSING_TRAFFICbefore anything is drained or destroyed.
Last verified against the Spring Boot 4.1.1 reference documentation (the current docs.spring.io version) and the Kubernetes documentation: 1 October 2026. The video was checked in September 2026 against the Boot 4.x docs, Boot 3.5.x source and Framework 6.2 source. Today we re-checked the availability states, probe tables, port binding, graceful shutdown and SIGTERM handling against the 4.x docs and the current Spring Boot source. One detail in the video needs a correction (see Change notes).
Scope: an executable JAR running Spring MVC on embedded Tomcat in a Kubernetes pod, Spring Boot 3 or 4. WebFlux, WAR deployment and native images take other paths and aren’t covered.
Reading the chart
A rounded box is a state; a box inside a box is a phase made of smaller steps. Arrows are labelled with the event or method that causes them. A diamond is a choice decided by a [condition]. Dashed regions run at the same time. One arrow leaving a big box applies to every state inside it, which is how the chart draws “any exception during startup fails startup” with a single arrow.
The chart reuses six memory pegs from the Spring Boot words primer. They’re analogies, memory aids, not definitions:
| Word | Memory peg (analogy) | What it actually is |
|---|---|---|
| ApplicationContext | the manager | The IoC container that creates and wires the beans |
| Bean | hired staff | An object the container creates and manages |
| Dependency injection | tools handed over | The container supplies constructor arguments |
| DispatcherServlet | the head waiter at the pass | Spring MVC’s front controller servlet |
@Transactional proxy | the tab supervisor | The proxy that begins, commits or rolls back the transaction |
| Started vs Ready | lights on vs doors open | Context refreshed vs runners done and readiness ACCEPTING_TRAFFIC |
1. Build: source code becomes an executable JAR
source code → compile → run tests → Boot repackage → executable JAR. If a test fails, the build stops and no jar is produced. The Spring Boot plugin repackages the jar: your classes go to BOOT-INF/classes, dependencies to BOOT-INF/lib, and Boot’s loader classes sit at the root. The manifest now reads:
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.mycompany.project.MyApplication
2. The container
The jar goes into an image and Kubernetes schedules a pod. The container runtime sets up the filesystem, volumes, secrets and config maps, environment variables, networking and CPU/memory limits. With the exec form of ENTRYPOINT (["java","-jar","app.jar"]), java runs as process 1, so it receives SIGTERM directly.
3. Inside the JVM: lifecycle and availability
Inside the JVM, after main() calls SpringApplication.run(), the chart has two parallel parts. The lifecycle moves from startup to serving to shutdown. Availability tracks what the probes see, following Spring Boot’s own probe tables:
| Phase | LivenessState | ReadinessState | HTTP server |
|---|---|---|---|
| Starting | BROKEN | REFUSING_TRAFFIC | Not started |
| Started | CORRECT | REFUSING_TRAFFIC | Refuses requests |
| Ready | CORRECT | ACCEPTING_TRAFFIC | Accepts requests |
| Graceful shutdown | CORRECT | REFUSING_TRAFFIC | New requests are rejected |
| Shutdown complete | N/A | N/A | Server is shut down |
Liveness has exactly two values, CORRECT and BROKEN; there is no STARTING. Spring Boot treats the app as live as soon as the context has been refreshed, and as ready as soon as the runners have been called. “Refuses requests” in Started doesn’t mean the port is closed: it’s already bound. It means the readiness probe keeps Kubernetes from routing traffic. The Boot 4.1 docs say the liveness and readiness health groups are enabled automatically and exposed at /actuator/health/liveness and /actuator/health/readiness.
A SIGKILL arrow leaves the whole JVM box: if shutdown outlasts the pod’s termination grace period (30 s by default), Kubernetes kills the process.
4. java -jar and JarLauncher
read MANIFEST.MF → JarLauncher → invoke main(). The Java launcher, not your code, reads Main-Class, which is JarLauncher, not your class. JarLauncher builds a class loader over BOOT-INF/classes and the nested jars in BOOT-INF/lib, loads Start-Class, and calls its main(args). A missing manifest or a class that won’t load fails the launch before any Spring code runs.
In MyApplication.main(), @SpringBootApplication is still just metadata; nothing has read it yet.
5. Startup: SpringApplication.run()
SpringApplication. First a choice, WebApplicationType: Spring MVC on the class path → SERVLET; only WebFlux → REACTIVE; neither → NONE. Then the events, in order:
ApplicationStartingEvent: only listeners and initializers are registered.- Prepare Environment: command-line arguments, environment variables, system properties and
application.ymlbecome property sources, and active profiles are decided, before any application bean exists. ThenApplicationEnvironmentPreparedEvent. - Create ApplicationContext (for a servlet app,
AnnotationConfigServletWebServerApplicationContext), created but not refreshed. Initializers run, thenApplicationContextInitializedEvent. - Your class is registered as the primary source,
ApplicationPreparedEventis published, andrefresh()begins.
refresh(): twelve fixed steps. prepareRefresh → obtainFreshBeanFactory → prepareBeanFactory → postProcessBeanFactory → invokeBeanFactoryPostProcessors → registerBeanPostProcessors → initMessageSource → initApplicationEventMulticaster → onRefresh → registerListeners → finishBeanFactoryInitialization → finishRefresh.
Configuration discovery (BeanFactory post-processors). ConfigurationClassPostProcessor parses @SpringBootApplication = @SpringBootConfiguration + @ComponentScan + @EnableAutoConfiguration. Scanning starts from your class’s package and produces bean definitions, not objects. Your own @Configuration/@Bean/@Import come next, and auto-configuration is imported last through a deferred import selector, so it can see your beans first. Each candidate in AutoConfiguration.imports is checked against its @Conditional annotations: it’s applied, or it backs off (a class is missing, or you defined the bean yourself).
onRefresh(): create the web server. Spring doesn’t look for “a Tomcat bean”. It looks for exactly one ServletWebServerFactory bean (Tomcat’s, by default). With none it throws MissingWebServerFactoryBeanException; with several, an ApplicationContextException naming them. The factory creates Tomcat (connectors, server.port, SSL, customizers), but Tomcat removes its connectors, so nothing binds yet. Two SmartLifecycle beans are registered: webServerStartStop and webServerGracefulShutdown.
Creating the beans. finishBeanFactoryInitialization creates every remaining non-lazy singleton in dependency order, not controller/service/repository order. For each: choose the constructor → resolve each argument by type (a missing dependency is created first) → constructor runs → @Autowired fields and @Value → Aware callbacks → @PostConstruct → afterPropertiesSet() / init-method → after initialization, wrap in a proxy if needed (@Transactional, @Async) → cache. If no bean matches, Spring throws UnsatisfiedDependencyException caused by NoSuchBeanDefinitionException. If several still match after @Primary / @Qualifier / parameter-name narrowing, the cause is NoUniqueBeanDefinitionException. @PostConstruct means this bean is ready, not the application.
finishRefresh(): Tomcat binds the port. The SmartLifecycle beans start. webServerStartStop calls webServer.start(), Tomcat adds its connectors back and binds, and ServletWebServerInitializedEvent is published (it carries the actual port). Only then is ContextRefreshedEvent published. If the port is taken, PortInUseException fails startup, after all your beans and their @PostConstruct methods already ran.
Started, runners, Ready. ApplicationStartedEvent is published and liveness becomes CORRECT; readiness still refuses traffic. ApplicationRunner / CommandLineRunner beans run in @Order sequence just before run() returns; a runner that throws still fails startup. Then ApplicationReadyEvent is published, and readiness becomes ACCEPTING_TRAFFIC.
Startup failed. Spring stops the lifecycle beans, destroys the singletons it created, publishes ApplicationFailedEvent, and a FailureAnalyzer prints APPLICATION FAILED TO START with a Description and an Action. The JVM exits with a non-zero status.
6. Serving
Three parallel regions:
- HTTP: Tomcat worker thread → filter chain (Spring Security can answer 401/403 here) → DispatcherServlet:
HandlerMapping(none → 404) → interceptors’preHandle(false → stop) → argument resolution (@Validfails → 400) → your@RestControllermethod →@Transactionalproxy (commit, or roll back onRuntimeException/Error, not checked exceptions) → return value written as JSON by anHttpMessageConverter. Exceptions go toHandlerExceptionResolvers(@ExceptionHandler,@ControllerAdvice). Filters unwind and the thread returns to the pool. - Message listener: receive, process, acknowledge, or retry/dead-letter.
- Scheduled jobs:
@Scheduledmethods run on a scheduler thread.
The chart groups listeners and scheduled jobs under “serving” for readability, but they start before readiness: listener containers are lifecycle beans started at the end of refresh, and scheduling registers on ContextRefreshedEvent. Messages can arrive before any HTTP traffic.
7. Graceful shutdown
Kubernetes runs the pod’s preStop hook first, then sends SIGTERM. The JVM shutdown hook that Spring Boot registered closes the context, in five steps:
context.close(): readiness is set toREFUSING_TRAFFICfirst.- Drain in-flight requests. Graceful shutdown runs in the earliest SmartLifecycle stop phase: Tomcat stops accepting new requests at the network layer and waits for active ones. The wait is bounded by
spring.lifecycle.timeout-per-shutdown-phase, 30 s by default. Graceful is the default (server.shutdown=graceful);server.shutdown=immediateturns it off. - Stop the remaining lifecycle beans, including the web server.
- Destroy singletons (
@PreDestroy,DisposableBean, destroy method) in reverse dependency order, so pools and clients close after the beans that use them. - Context closed, shutdown hooks complete, the JVM exits.
# Spring Boot's documented preStop example (Kubernetes 1.32+)
lifecycle:
preStop:
sleep:
seconds: 10
Why the sleep: when Kubernetes deletes a pod, the shutdown hooks and the removal from load balancers happen in parallel, so traffic can still arrive briefly. The Spring Boot docs recommend a preStop sleep at least as long as your longest in-flight request. If preStop plus shutdown can exceed 30 seconds, for example because you raised the shutdown timeout, raise terminationGracePeriodSeconds too. Otherwise SIGKILL ends the process mid-shutdown.
Pause & Prove
1. Your app logs “Started MyApplication”, but Kubernetes still isn’t sending it traffic. Which probe is holding it back, and what is the app waiting for?
- A. Liveness, until the port binds. Liveness is already
CORRECTat Started, and the port bound earlier, infinishRefresh(). - B. Readiness, until the runners finish. ✓ After
ApplicationStartedEvent, readiness is stillREFUSING_TRAFFIC. It switches toACCEPTING_TRAFFICright after the runners finish andApplicationReadyEventis published. - C. Readiness, until the first request warms up the DispatcherServlet. No request can arrive through the Service before readiness passes.
- D. Neither; it’s a Kubernetes delay. The probe tables show readiness refusing traffic at Started.
2. Spring Boot creates embedded Tomcat during refresh(). When does Tomcat bind the port? (Community poll)
- In
onRefresh(). Tempting: that’s where Tomcat is created, but its connectors are removed so nothing binds. - In
finishRefresh(). ✓webServerStartStopstarts the server, connectors are added back and bound. That’s also where “Port 8080 was already in use” comes from. - After
ApplicationReadyEvent. Too late: the port is bound before evenContextRefreshedEvent. - On the first HTTP request. Tomcat binds at startup, not lazily.
3. Your @PostConstruct ran, and startup still failed on the port. Why did it run first?
Because singletons are created in finishBeanFactoryInitialization(), which comes before finishRefresh(), where the port is bound.
4. Which happens first on shutdown: readiness REFUSING_TRAFFIC or @PreDestroy?
Readiness REFUSING_TRAFFIC. It’s published at the start of context.close(), before the drain, the lifecycle stop, and singleton destruction.
5. Peg drill
ApplicationContext → the manager · bean → hired staff · DispatcherServlet → the head waiter at the pass · Started vs Ready → lights on vs doors open.
Related Shorts
- “Started” isn’t “Ready” in Spring Boot
- Where “Port 8080 was already in use” actually fails
- What your Spring Boot app does when Kubernetes sends SIGTERM
Before / after this video
- Before (primers): Port, Socket, Reverse Proxy, JVM, Pod, SIGTERM: Backend Words and Spring Boot Words Explained. Already know them? Start here.
- Next: How Apache Tomcat Handles a Request, the server inside this chart, drawn as its own state machine.
Sources
- Spring Boot, Kubernetes probes: Application Lifecycle and Probe States: startup and shutdown probe tables, health groups auto-enabled, probe URLs (re-checked 1 Oct 2026, Boot 4.1.1)
- Spring Boot, SpringApplication: availability states, event order (incl.
WebServerInitializedEventandContextRefreshedEventbetween Prepared and Started), runners, shutdown hook, FailureAnalyzer (re-checked 1 Oct 2026) - Spring Boot, Graceful Shutdown: on by default, earliest SmartLifecycle stop phase, network-layer rejection,
server.shutdown=immediate(re-checked 1 Oct 2026) - Spring Boot, Common Application Properties:
spring.lifecycle.timeout-per-shutdown-phasedefault 30s,server.shutdowndefault graceful,server.port8080 (re-checked 1 Oct 2026) - Spring Boot, Kubernetes Container Lifecycle: parallel shutdown, preStop sleep, SIGTERM after preStop, grace period (re-checked 1 Oct 2026)
- Spring Boot, Launching Executable Jars and Nested JARs:
Main-Class/Start-Class,BOOT-INF/classesandBOOT-INF/lib(re-checked 1 Oct 2026) - Spring Boot, Using @SpringBootApplication, Auto-configuration, Developing Auto-configuration: the three annotations, candidates, conditions, back-off
- Spring Boot source (main branch, 4.2 line):
ServletWebServerApplicationContext(factory lookup, lifecycle beans,doClosepublishesREFUSING_TRAFFIC),WebServerStartStopLifecycle(start, thenServletWebServerInitializedEvent),TomcatWebServer(connectors removed on create, re-added and bound instart(),PortInUseException), checked 1 Oct 2026 - Spring Framework,
AbstractApplicationContextandConstructorResolver: refresh steps, lifecycle start beforeContextRefreshedEvent,UnsatisfiedDependencyExceptionwrapping - Spring Framework, Customizing the Nature of a Bean: init and destroy callback order
- Spring Framework, DispatcherServlet, Using @Transactional, Rollback rules: request path, proxy and self-invocation, default rollback
- Spring for Apache Kafka, @KafkaListener lifecycle and
ScheduledAnnotationBeanPostProcessor: listeners and scheduling start before readiness - Kubernetes, Pod termination: preStop runs, TERM to process 1, 30 s default grace period, then KILL (re-checked 1 Oct 2026)
- Docker, Dockerfile ENTRYPOINT: exec form runs the program as PID 1
Change notes
- 1 Oct 2026: first published.
- Correction (1 Oct 2026): the video’s availability region shows liveness returning to
BROKEN“when the context closes”. Spring Boot’s shutdown table lists liveness and readiness as N/A once shutdown is complete (the probes stop answering), and Spring Boot’s close path publishes onlyReadinessState.REFUSING_TRAFFIC, notLivenessState.BROKEN. This page follows the docs. The rest of the shutdown order in the video is unchanged.
Not affiliated with or endorsed by VMware Broadcom (Spring) or the Apache Software Foundation. Spring is a trademark of Broadcom Inc. and/or its subsidiaries; Apache Tomcat is a trademark of the Apache Software Foundation. Found a mistake? Tell us in the video’s comments and we’ll correct this page.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →Prefer email? Get the free newsletter: one failure, traced step by step, about once a week.