Skip to content

How to Migrate from MicroProfile Health to Spring Boot Actuator

Problem

I was migrating a Java microservices application from Quarkus to Spring Boot. Everything was going smoothly until I hit the health check endpoints. My Kubernetes probes started failing with 404 errors, and my custom health checks were nowhere to be found.

Kubernetes Events
Warning Unhealthy 2m (x10 over 3m) kubelet Liveness probe failed: HTTP status 404
Warning Unhealthy 2m (x10 over 3m) kubelet Readiness probe failed: HTTP status 404

The old application used MicroProfile Health with @Liveness, @Readiness, and @Startup annotations. Spring Boot uses a completely different approach with HealthIndicator beans and the Actuator framework.

Environment

  • Spring Boot 3.x
  • Kubernetes 1.28
  • Java 17
  • Previously: Quarkus 3.x with MicroProfile Health

The Migration Challenge

MicroProfile Health and Spring Boot Actuator serve the same purpose but use different implementations. Here’s the mapping I discovered:

Concept Mapping
MicroProfile Spring Boot Actuator
------------- --------------------
HealthCheck interface -> HealthIndicator interface
@Liveness annotation -> HealthIndicator bean (liveness group)
@Readiness annotation -> HealthIndicator bean (readiness group)
@Startup annotation -> HealthIndicator bean (startup group)
/health/live -> /actuator/health/liveness
/health/ready -> /actuator/health/readiness
HealthCheckResponse -> Health class

Before: MicroProfile Health Check

This was my original database health check in Quarkus:

DatabaseCheck.java
@Readiness
@ApplicationScoped
public class DatabaseCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
return HealthCheckResponse.named("database")
.withData("connection", "active")
.withData("poolSize", "10")
.up()
.build();
}
}

The @Readiness annotation automatically registered this as a readiness probe. I also had a liveness check:

AppLivenessCheck.java
@Liveness
@ApplicationScoped
public class AppLivenessCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
return HealthCheckResponse.named("app-liveness")
.up()
.build();
}
}

After: Spring Boot Health Indicator

After migration, I converted these to Spring Boot HealthIndicator beans:

DatabaseHealthIndicator.java
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Override
public Health health() {
return Health.up()
.withDetail("connection", "active")
.withDetail("poolSize", "10")
.build();
}
}
AppLivenessHealthIndicator.java
@Component
public class AppLivenessHealthIndicator implements HealthIndicator {
@Override
public Health health() {
return Health.up().build();
}
}

But wait, how does Spring Boot know which one is liveness and which one is readiness?

Solution

Step 1: Configure Health Groups

Spring Boot 2.2+ supports health groups for liveness and readiness probes. I added this configuration:

application.yml
management:
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
startupstate:
enabled: true

Wait, but how do I assign my custom HealthIndicator beans to specific groups?

Step 2: Auto-Registration vs Manual Group Assignment

I discovered two approaches:

Approach A: Use Built-in Health Contributors

Spring Boot automatically includes LivenessStateHealthIndicator and ReadinessStateHealthIndicator when you enable the probes. These check the application’s lifecycle state:

HealthController.java
@RestController
public class HealthController {
private final ApplicationEventPublisher eventPublisher;
public HealthController(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@PostMapping("/ready")
public void markReady() {
eventPublisher.publishEvent(new ReadinessStateChangedEvent(this, ReadinessState.ACCEPTING_TRAFFIC));
}
@PostMapping("/not-ready")
public void markNotReady() {
eventPublisher.publishEvent(new ReadinessStateChangedEvent(this, ReadinessState.REFUSING_TRAFFIC));
}
}

Approach B: Custom Health Indicators in Groups

For custom health checks that should affect readiness, I configured them explicitly:

application.yml
management:
endpoint:
health:
show-details: when-authorized
group:
readiness:
include: "databaseHealthIndicator,redisHealthIndicator"
liveness:
include: "pingHealthIndicator"

Step 3: Update Kubernetes Probe Paths

This was the critical step I initially missed. The endpoint paths changed:

kubernetes-deployment.yml
# BEFORE (MicroProfile)
livenessProbe:
httpGet:
path: /health/live
port: 8080
readinessProbe:
httpGet:
path: /health/ready
port: 8080
# AFTER (Spring Boot Actuator)
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
startupProbe:
httpGet:
path: /actuator/health/startup
port: 8080

Step 4: Configure Security

Here’s where things got tricky. Spring Boot Actuator endpoints are secured by default. My probes were now returning 401 Unauthorized instead of 404 Not Found:

Kubernetes Events
Warning Unhealthy 30s kubelet Liveness probe failed: HTTP status 401

I had to configure security to allow anonymous access to health endpoints:

SecurityConfig.java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}

Alternatively, you can configure actuator to expose health without authentication:

application.yml
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-details: never

Common Mistakes

I made several mistakes during this migration:

  1. Forgot to update probe paths: The Kubernetes deployment still pointed to /health/live instead of /actuator/health/liveness.

  2. Missing actuator dependency: I had to add the actuator starter:

build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
  1. Security blocked probes: Actuator endpoints return 401 by default. I needed to permit unauthenticated access to health endpoints.

  2. Wrong health indicator naming: Spring Boot uses HealthIndicator suffix convention. My DatabaseCheck needed to become DatabaseHealthIndicator.

  3. Missing health group configuration: Without explicit group configuration, all health indicators contribute to the main health endpoint, not liveness/readiness probes.

Complete Working Example

Here’s my final configuration for a complete migration:

CustomHealthIndicator.java
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(2)) {
return Health.up()
.withDetail("database", "PostgreSQL")
.withDetail("validationQuery", "SELECT 1")
.build();
}
} catch (SQLException e) {
return Health.down()
.withException(e)
.withDetail("error", e.getMessage())
.build();
}
return Health.down().withDetail("reason", "Unknown").build();
}
}
application.yml
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
group:
readiness:
include: "databaseHealthIndicator"
k8s-deployment.yml
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5

Summary

Migrating from MicroProfile Health to Spring Boot Actuator requires understanding the conceptual mapping between the two frameworks. The key steps are:

  1. Replace HealthCheck interface with HealthIndicator interface
  2. Convert @Liveness, @Readiness, @Startup annotations to @Component beans with health groups
  3. Update HealthCheckResponse builder to Spring’s Health builder
  4. Change Kubernetes probe paths from /health/* to /actuator/health/*
  5. Configure security to permit anonymous access to health endpoints

The concepts transfer well - liveness, readiness, and startup probes exist in both frameworks. The implementation differs, but the mental model remains the same. Once you understand the mapping, the migration is straightforward.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments