Spring Boot
Health Checks
Health checks report whether a service is capable of handling requests. Spring Boot Actuator provides /actuator/health with built-in indicators for databases, caches, message brokers, and disk space. Kubernetes uses two specialised probes — liveness (should the container be restarted?) and readiness (should traffic be routed to this instance?) — which map directly to Spring Boot's health probe endpoints.
Built-in Health Indicators
Spring Boot auto-configures health indicators for every infrastructure component it detects on the classpath. Each indicator makes a lightweight check — a SELECT 1 for databases, a PING for Redis, a metadata request for Kafka — and contributes UP or DOWN to the aggregate health status. The aggregate is DOWN if any single indicator is DOWN.
yaml
# application.yml
management:
endpoint:
health:
show-details: always # always|never|when-authorized
show-components: always # show individual component statuses
# ── GET /actuator/health response: ───────────────────────────────────
# {
# "status": "UP",
# "components": {
# "db": {
# "status": "UP",
# "details": {
# "database": "PostgreSQL",
# "validationQuery": "isValid()"
# }
# },
# "redis": {
# "status": "UP",
# "details": { "version": "7.0.11" }
# },
# "kafka": {
# "status": "UP",
# "details": {
# "bootstrapServers": "kafka:9092"
# }
# },
# "diskSpace": {
# "status": "UP",
# "details": {
# "total": 107374182400,
# "free": 52698873856,
# "threshold": 10485760
# }
# }
# }
# }
# ── Auto-configured indicators (activate when dependency detected): ────
# DataSourceHealthIndicator → spring-boot-starter-data-jpa
# RedisHealthIndicator → spring-boot-starter-data-redis
# KafkaHealthIndicator → spring-kafka
# MongoHealthIndicator → spring-boot-starter-data-mongodb
# ElasticsearchHealthIndicator → spring-boot-starter-data-elasticsearch
# RabbitHealthIndicator → spring-boot-starter-amqp
# DiskSpaceHealthIndicator → always active
# PingHealthIndicator → always active (trivial UP check)
# ── Disable a specific indicator: ────────────────────────────────────
# management:
# health:
# kafka:
# enabled: false # exclude Kafka from aggregate healthLiveness and Readiness Probes
Liveness answers "is this process alive and not stuck in a deadlock?" — if it fails, Kubernetes restarts the container. Readiness answers "can this instance handle traffic right now?" — if it fails, Kubernetes removes it from the load balancer but does not restart it. These are different questions and must never be combined into a single probe.
yaml
# application.yml — enable probes: ──────────────────────────────────
# management:
# endpoint:
# health:
# probes:
# enabled: true
# health:
# livenessstate:
# enabled: true
# readinessstate:
# enabled: true
# GET /actuator/health/liveness → { "status": "UP" } or "DOWN"
# GET /actuator/health/readiness → { "status": "UP" } or "OUT_OF_SERVICE"
# ── Kubernetes deployment probes: ────────────────────────────────────
# apiVersion: apps/v1
# kind: Deployment
# spec:
# template:
# spec:
# containers:
# - name: order-service
# image: order-service:latest
# ports:
# - containerPort: 8082
#
# livenessProbe:
# httpGet:
# path: /actuator/health/liveness
# port: 9090 # management port
# initialDelaySeconds: 30 # wait 30s after start
# periodSeconds: 10 # check every 10s
# failureThreshold: 3 # restart after 3 failures
# timeoutSeconds: 3
#
# readinessProbe:
# httpGet:
# path: /actuator/health/readiness
# port: 9090
# initialDelaySeconds: 15
# periodSeconds: 5
# failureThreshold: 3 # remove from LB after 3 failures
# successThreshold: 1 # re-add after 1 success
# timeoutSeconds: 3
// ── Programmatically control readiness state: ─────────────────────────
// Useful for graceful shutdown or maintenance mode.
@Service
@RequiredArgsConstructor
public class MaintenanceModeService {
private final ApplicationEventPublisher eventPublisher;
public void enterMaintenanceMode() {
// Remove from load balancer without restarting:
eventPublisher.publishEvent(
new AvailabilityChangeEvent<>(this,
ReadinessState.REFUSING_TRAFFIC));
log.info("Service entered maintenance mode — " +
"readiness probe now returns OUT_OF_SERVICE");
}
public void exitMaintenanceMode() {
eventPublisher.publishEvent(
new AvailabilityChangeEvent<>(this,
ReadinessState.ACCEPTING_TRAFFIC));
log.info("Service exited maintenance mode — " +
"readiness probe now returns UP");
}
public void signalLivenessFailure() {
// Trigger container restart via liveness probe:
eventPublisher.publishEvent(
new AvailabilityChangeEvent<>(this,
LivenessState.BROKEN));
}
}Custom Health Indicators
Custom HealthIndicators check dependencies that Spring Boot does not know about — third-party REST APIs, custom connection pools, license servers, or any external system the service depends on. A DOWN indicator signals that the service cannot operate correctly, which can remove it from the load balancer or alert on-call engineers.
Java
// ── Custom indicator — external payment gateway: ─────────────────────
@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentGatewayClient paymentClient;
@Override
public Health health() {
try {
long start = System.currentTimeMillis();
GatewayStatusResponse status = paymentClient.ping();
long latency = System.currentTimeMillis() - start;
if (status.isOperational()) {
return Health.up()
.withDetail("gateway", status.getName())
.withDetail("latency_ms", latency)
.withDetail("environment", status.getEnvironment())
.build();
} else {
return Health.down()
.withDetail("gateway", status.getName())
.withDetail("reason", status.getStatusMessage())
.build();
}
} catch (Exception ex) {
log.warn("Payment gateway health check failed: {}",
ex.getMessage());
return Health.down()
.withDetail("error", ex.getMessage())
.withException(ex)
.build();
}
}
}
// ── Composite health indicator — group related checks: ───────────────
@Component("externalDependencies")
public class ExternalDependenciesHealthIndicator
implements CompositeHealthContributor {
private final Map<String, HealthContributor> contributors;
public ExternalDependenciesHealthIndicator(
PaymentGatewayHealthIndicator paymentHealth,
ShippingApiHealthIndicator shippingHealth,
FraudDetectionHealthIndicator fraudHealth) {
this.contributors = Map.of(
"payment-gateway", paymentHealth,
"shipping-api", shippingHealth,
"fraud-detection", fraudHealth
);
}
@Override
public Iterator<NamedContributor<HealthContributor>> iterator() {
return contributors.entrySet().stream()
.map(e -> NamedContributor.of(e.getKey(), e.getValue()))
.iterator();
}
@Override
public HealthContributor getContributor(String name) {
return contributors.get(name);
}
}
// ── GET /actuator/health response with composite: ─────────────────────
// {
// "status": "UP",
// "components": {
// "externalDependencies": {
// "status": "UP",
// "components": {
// "payment-gateway": { "status": "UP", "details": {...} },
// "shipping-api": { "status": "UP", "details": {...} },
// "fraud-detection": { "status": "DOWN", "details": {...} }
// }
// }
// }
// }Graceful Shutdown and Health
During a rolling deployment Kubernetes sends SIGTERM to the pod before terminating it. Spring Boot's graceful shutdown allows in-flight requests to complete before the JVM exits. The readiness probe must report OUT_OF_SERVICE as soon as SIGTERM is received so the load balancer stops routing new requests while existing ones finish.
yaml
# application.yml — enable graceful shutdown: ────────────────────────
# server:
# shutdown: graceful # wait for in-flight requests
#
# spring:
# lifecycle:
# timeout-per-shutdown-phase: 30s # max wait before force-kill
// ── Graceful shutdown sequence: ───────────────────────────────────────
//
// t=0 Kubernetes sends SIGTERM
// t=0 Spring Boot sets readiness → OUT_OF_SERVICE
// t=0 Kubernetes readiness probe fails → pod removed from LB
// t=1 No new requests routed to this pod
// t=1 In-flight requests continue to be processed
// t=30 All in-flight requests complete (or timeout-per-shutdown-phase)
// t=30 Spring context shuts down (beans destroyed, DB pools closed)
// t=31 JVM exits with code 0
//
// Kubernetes terminationGracePeriodSeconds must be > Spring timeout:
// spec:
// terminationGracePeriodSeconds: 60 # > Spring's 30s
// ── Custom shutdown hook — log active request count: ─────────────────
@Component
@RequiredArgsConstructor
@Slf4j
public class GracefulShutdownLogger {
private final AtomicInteger activeRequests = new AtomicInteger(0);
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
log.info("Graceful shutdown initiated — " +
"active requests: {}", activeRequests.get());
// Spring Boot will wait for these to finish (server.shutdown=graceful)
}
@EventListener(AvailabilityChangeEvent.class)
public void onAvailabilityChange(AvailabilityChangeEvent<?> event) {
log.info("Availability state changed to: {}",
event.getState());
}
}