Definition
- SpringApplication.run() – the entry point that creates the IoC container, triggers auto-configuration, instantiates singleton beans, and starts the embedded server.
- ApplicationContext – the IoC container that holds all beans and their dependencies, using
AnnotationConfigServletWebServerApplicationContextfor web applications. - DispatcherServlet – the front controller that receives every HTTP request and delegates to the correct controller method via HandlerMapping.
- CommandLineRunner / ApplicationRunner – beans that execute one-time tasks (cache warming, data loading) after the context is fully ready.
- Graceful shutdown – stops accepting new connections while draining in-flight requests within a configurable timeout before the server exits.
Opening a National Bank of India branch: the manager arrives (SpringApplication.run), sets up equipment based on what is available (auto-configuration), calls staff to their desks in dependency order (bean creation), opens the front door (Tomcat starts), and runs morning tasks like loading the cash drawer (CommandLineRunner). Closing for renovation: stop letting new customers in, finish serving everyone already inside, then lock the doors.
Understanding the Concepts
What is the Spring Boot startup flow?
The Spring Boot startup sequence has eight steps that execute in a specific order. Understanding this order helps you debug startup failures and know exactly when your beans, controllers, and scheduled tasks become active.
- The
mainmethod callsSpringApplication.run(MainClass.class, args). - Spring creates the
ApplicationContext(the IoC container). For web applications this isAnnotationConfigServletWebServerApplicationContext. - Auto-configuration kicks in: Spring Boot scans the classpath, finds libraries like Tomcat, JPA, and Redis, and registers matching
@Configurationclasses. @ComponentScandiscovers all@Component,@Service,@Repository, and@Controllerclasses and registers them as bean definitions.- Singleton beans are instantiated in dependency order: constructor injection first, then
@PostConstructlifecycle callbacks. - The embedded web server (Tomcat by default) is created and started.
CommandLineRunnerandApplicationRunnerbeans execute their one-time tasks.ApplicationReadyEventis published: the application is fully started and ready to serve requests.
package com.bank;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NationalBankApplication {
public static void main(String[] args) {
// Step 1: triggers the entire startup sequence
SpringApplication.run(NationalBankApplication.class, args);
}
}
package com.bank.startup;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class CacheWarmupRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(CacheWarmupRunner.class);
private final ExchangeRateService rateService;
public CacheWarmupRunner(ExchangeRateService rateService) {
this.rateService = rateService;
}
@Override
public void run(String... args) {
// Step 7: executes after all beans are created and server is started
log.info("Warming exchange rate cache for National Bank of India");
rateService.loadAllRates();
log.info("Cache warmup complete");
}
}
2024-01-15 09:00:01 INFO NationalBankApplication : Starting NationalBankApplication
2024-01-15 09:00:03 INFO o.s.b.w.e.t.TomcatWebServer : Tomcat started on port 8080
2024-01-15 09:00:03 INFO CacheWarmupRunner : Warming exchange rate cache for National Bank of India
2024-01-15 09:00:04 INFO CacheWarmupRunner : Cache warmup complete
2024-01-15 09:00:04 INFO NationalBankApplication : Started in 3.2 seconds
What is DispatcherServlet and how does it route requests?
DispatcherServlet is the front controller in Spring MVC. Every HTTP request that reaches the embedded Tomcat server goes through the DispatcherServlet. It does not handle the request itself. Instead, it delegates to the correct controller method using a series of collaborators.
The request flow through DispatcherServlet has four stages:
- HandlerMapping finds the controller method that matches the request URL and HTTP method.
- HandlerAdapter invokes that controller method, converting path variables, request parameters, and the JSON body into method arguments.
- The controller method returns a result (a DTO, a ResponseEntity, or a view name).
- HttpMessageConverter (Jackson by default) serializes the result to JSON and writes it to the HTTP response.
package com.bank.controller;
import com.bank.model.Account;
import com.bank.service.AccountService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
@RequestMapping("/api/v1/accounts")
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
// DispatcherServlet routes GET /api/v1/accounts/SAV2001 here
@GetMapping("/{accountId}")
public ResponseEntity<Account> getAccount(@PathVariable String accountId) {
Account account = accountService.findById(accountId);
return ResponseEntity.ok(account);
}
}
HTTP Request: GET /api/v1/accounts/SAV2001
│
▼
DispatcherServlet
│
├── HandlerMapping: matches AccountController.getAccount()
├── HandlerAdapter: calls method with accountId = "SAV2001"
├── Controller returns Account object
└── Jackson converts Account to JSON
│
▼
HTTP Response: 200 OK {"accountId": "SAV2001", "balance": 150000.00}
How do you build and run a Spring Boot application?
Spring Boot produces a fat JAR (also called an uber JAR) that contains your compiled classes, all dependencies, and the embedded Tomcat server. You can run it anywhere a JVM is installed.
# Build the fat JAR with Maven
mvn clean package -DskipTests
# Run the fat JAR
java -jar target/national-bank-app-1.0.0.jar
# Run with a specific profile
java -jar target/national-bank-app-1.0.0.jar --spring.profiles.active=prod
# Build a Docker image using Spring Boot's built-in buildpack support
mvn spring-boot:build-image
$ java -jar target/national-bank-app-1.0.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.1)
Started NationalBankApplication in 3.2 seconds
What is graceful shutdown and why does it matter?
Graceful shutdown changes how the server stops. Without it, when the server receives a SIGTERM signal (the standard termination signal from the operating system or Kubernetes), it stops immediately and drops any requests that are still being processed. With graceful shutdown enabled, the server stops accepting new connections but continues processing in-flight requests for up to a configurable timeout.
This matters in production because Priya, the customer, might be in the middle of a fund transfer when a deployment happens. Without graceful shutdown, her transfer request could be dropped mid-way.
# application.yml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
package com.bank.service;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class TransferService {
private static final Logger log = LoggerFactory.getLogger(TransferService.class);
// This runs during graceful shutdown after in-flight requests complete
@PreDestroy
public void cleanup() {
log.info("Closing database connections and flushing audit log");
}
}
Graceful shutdown timeline:
1. SIGTERM received
2. Server stops accepting NEW connections
3. In-flight requests continue processing (up to 30 seconds)
4. All in-flight requests complete
5. @PreDestroy callbacks execute (close connections, flush buffers)
6. ApplicationContext closes
7. JVM exits
How does zero-downtime deployment work in Kubernetes?
In Kubernetes, zero-downtime deployment combines graceful shutdown with readiness probes and a preStop hook. The preStop hook adds a delay before SIGTERM so the pod is removed from the service endpoints first, preventing new requests from reaching a terminating pod.
# Kubernetes deployment manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: national-bank-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: national-bank-app:1.0.0
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
Kubernetes rolling update flow:
1. New pod starts, passes readiness probe
2. Old pod receives preStop hook -> sleeps 5 seconds
3. K8s removes old pod from Service endpoints (no new traffic)
4. SIGTERM sent to old pod
5. Graceful shutdown: drain in-flight requests (30 seconds max)
6. Old pod exits cleanly
7. Zero requests dropped
What are common mistakes with Spring Boot startup?
The most common mistake is putting heavy initialization logic in a bean constructor or @PostConstruct method, which blocks the entire startup and delays the application from becoming ready.
// WRONG: heavy work in @PostConstruct blocks startup
package com.bank.service;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
@Service
public class SlowStartupService {
@PostConstruct
public void init() {
// This blocks startup for 30 seconds
// All other beans wait, Tomcat does not start
loadMillionRecordsFromDatabase();
}
}
// CORRECT: use CommandLineRunner for heavy startup tasks
package com.bank.startup;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DataLoaderRunner implements CommandLineRunner {
private final DataLoadService dataLoadService;
public DataLoaderRunner(DataLoadService dataLoadService) {
this.dataLoadService = dataLoadService;
}
@Override
public void run(String... args) {
// Runs after Tomcat starts, does not block other beans
dataLoadService.loadRecordsAsync();
}
}
Another mistake is not configuring readiness probes. Without a readiness probe, Kubernetes sends traffic to a pod that is still starting up, causing 503 errors for Priya.
What are the differences between liveness and readiness probes?
| Probe | Purpose | Failure Action | What to Include |
|---|---|---|---|
| Liveness | Is the JVM alive? | Kubernetes restarts the pod | Only internal JVM health |
| Readiness | Can the app serve traffic? | Kubernetes removes from load balancer | DB connectivity, cache, external services |
Pankaj designed the probes at National Bank of India with a critical rule: never include external dependencies in the liveness probe. If the database goes down, the liveness probe should still pass. A database outage should not cause pod restarts, which would make the situation worse. External dependencies belong only in the readiness probe.
package com.bank.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CoreBankingHealthIndicator implements HealthIndicator {
private final CoreBankingClient coreBankingClient;
public CoreBankingHealthIndicator(CoreBankingClient coreBankingClient) {
this.coreBankingClient = coreBankingClient;
}
@Override
public Health health() {
// This affects readiness, NOT liveness
boolean isReachable = coreBankingClient.ping();
if (isReachable) {
return Health.up().withDetail("coreBanking", "reachable").build();
}
return Health.down().withDetail("coreBanking", "unreachable").build();
}
}
What should a senior engineer know?
- The startup order matters for debugging
BeanCurrentlyInCreationException. Circular dependencies (A depends on B, B depends on A) fail at startup. The fix is to redesign the dependency or use@Lazyon one injection point. ApplicationReadyEventis the safest place to trigger post-startup logic because all beans are fully initialized and the server is accepting traffic.- In a microservices architecture, Pankaj configures the
spring.main.banner-mode=offin production to reduce log noise and setsspring.jmx.enabled=falseto reduce memory overhead. - The embedded server is not limited to Tomcat. You can switch to Jetty or Undertow by excluding
spring-boot-starter-tomcatand adding the alternative starter.
package com.bank.listener;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class StartupNotifier {
private static final Logger log = LoggerFactory.getLogger(StartupNotifier.class);
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
log.info("National Bank of India application is fully started and ready");
}
}
What are common interview follow-up questions?
- What is the difference between
CommandLineRunnerandApplicationRunner? Both run after startup.CommandLineRunnerreceives rawString[] args.ApplicationRunnerreceives parsedApplicationArgumentswith option support. - Can you have multiple
CommandLineRunnerbeans? Yes. Use@Orderto control their execution sequence. - What happens if a
CommandLineRunnerthrows an exception? The application fails to start and exits. Wrap risky logic in try-catch if the task is non-critical. - How do you change the embedded server from Tomcat to Jetty? Exclude
spring-boot-starter-tomcatfromspring-boot-starter-weband addspring-boot-starter-jetty.
<!-- pom.xml: switch from Tomcat to Jetty --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-websyn-kw"></artifactIdsyn-kw">>
syn-kw"><exclusionssyn-kw">>
syn-kw"><exclusionsyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-tomcatsyn-kw"></artifactIdsyn-kw">>
syn-kw"></exclusionsyn-kw">>
syn-kw"></exclusionssyn-kw">>
syn-kw"></dependencysyn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-jettysyn-kw"></artifactIdsyn-kw">>
syn-kw"></dependencysyn-kw">>
Visual Overview
Spring Boot Startup Sequence:
┌─────────────────────────────────┐
│ main() calls │
│ SpringApplication.run() │
└────────────┬────────────────────┘
▼
┌─────────────────────────────────┐
│ Create ApplicationContext │
│ (IoC container) │
└────────────┬────────────────────┘
▼
┌─────────────────────────────────┐
│ Auto-configuration │
│ (scan classpath, match conds) │
└────────────┬────────────────────┘
▼
┌─────────────────────────────────┐
│ @ComponentScan │
│ (discover @Service, @Controller)│
└────────────┬────────────────────┘
▼
┌─────────────────────────────────┐
│ Bean instantiation │
│ (dependency order, @PostConstruct)│
└────────────┬────────────────────┘
▼
┌─────────────────────────────────┐
│ Start embedded Tomcat │
│ Register DispatcherServlet │
└────────────┬────────────────────┘
▼
┌─────────────────────────────────┐
│ Run CommandLineRunner / │
│ ApplicationRunner beans │
└────────────┬────────────────────┘
▼
┌─────────────────────────────────┐
│ Publish ApplicationReadyEvent │
│ Application is READY │
└─────────────────────────────────┘
DispatcherServlet Request Flow:
HTTP Request
│
▼
┌─────────────────┐
│ DispatcherServlet│
└────────┬────────┘
▼
┌─────────────────┐
│ HandlerMapping │ → finds matching @Controller method
└────────┬────────┘
▼
┌─────────────────┐
│ HandlerAdapter │ → invokes the method
└────────┬────────┘
▼
┌──────────────────────┐
│ HttpMessageConverter │ → serializes result to JSON
└────────┬─────────────┘
▼
HTTP Response
Key Takeaway
Spring Boot starts when the main method calls SpringApplication.run, which creates the ApplicationContext, triggers auto-configuration based on the classpath, discovers components via scanning, instantiates all singleton beans in dependency order, starts the embedded Tomcat, and runs CommandLineRunner hooks. The DispatcherServlet is the front controller that routes every HTTP request through HandlerMapping to find the matching controller method, through HandlerAdapter to invoke it, and through HttpMessageConverter to serialize the response as JSON.
For zero-downtime deployment in Kubernetes, Pankaj configures graceful shutdown with a 30-second timeout. When Kubernetes sends SIGTERM during a rolling update, the preStop hook waits 5 seconds for the load balancer to deregister the pod, Spring stops accepting new connections, in-flight requests complete within the timeout, PreDestroy callbacks close connections and flush buffers, and the pod exits cleanly. New pods pass readiness probes before old ones shut down, ensuring Priya never sees a dropped request during deployment.
The critical rule for health probes is to separate liveness from readiness. Liveness checks only internal JVM health. Readiness checks external dependencies like the database and core banking system. A database outage should remove the pod from the load balancer, not restart it.
Remember one sentence: SpringApplication.run creates the context, auto-configures from the classpath, starts Tomcat, and runs your startup tasks, while graceful shutdown ensures no request is ever dropped during deployment.
---