Spring BootHealth Checks
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 health

Liveness 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());
    }
}