Kubernetes Readiness Probes in Spring Boot Microservices: Designing a Production-Ready Health Check

How to Calculate Application Readiness Using Database, Redis, Solace, Certificates and Startup State

In a Kubernetes or OpenShift environment, a microservice being running does not necessarily mean it is ready to receive traffic.

A Spring Boot application may have successfully started its embedded Tomcat server while:

  • The database connection is still being initialized.
  • Redis is not available.
  • Solace messaging is disconnected.
  • Required certificates have not been loaded.
  • Application configuration has not finished loading.
  • Required caches have not been populated.
  • Downstream dependencies are temporarily unavailable.

If Kubernetes starts sending traffic too early, users may receive errors even though the pod itself appears to be running.

This is exactly the problem that Kubernetes Readiness Probes are designed to solve.


1. Liveness vs Readiness

The first concept to understand is that liveness and readiness have different purposes.

ProbeQuestionFailure Action
LivenessIs the application alive?Kubernetes may restart the container
ReadinessCan the application handle traffic?Kubernetes removes the pod from Service endpoints
StartupHas the application finished starting?Prevents premature liveness/readiness checks

A useful mental model is:

                    Kubernetes Pod
                         |
              +----------+----------+
              |                     |
          Liveness              Readiness
              |                     |
       "Am I alive?"       "Can I serve traffic?"
              |                     |
          Restart             Receive traffic
          if broken            if healthy

2. Why /actuator/info Should Not Be Used

A common configuration is:

livenessProbe:
  httpGet:
    path: /actuator/info
    port: 8080

readinessProbe:
  httpGet:
    path: /actuator/info
    port: 8080

This technically works because Kubernetes only cares about the HTTP response.

However, it is not a good health-check design.

/actuator/info is intended to expose application information such as:

{
  "app": {
    "name": "order-service",
    "version": "1.2.5"
  }
}

It does not answer:

“Can this application safely receive production traffic?”

The application could return HTTP 200 from /actuator/info while:

Oracle       DOWN
Redis        DOWN
Solace       DISCONNECTED
Certificates NOT LOADED

Kubernetes would still consider the pod ready.


3. Spring Boot’s Built-in Kubernetes Health Endpoints

Spring Boot provides dedicated health groups for Kubernetes.

The recommended endpoints are:

/actuator/health/liveness

and:

/actuator/health/readiness

Enable Kubernetes probe support:

management:
  endpoint:
    health:
      probes:
        enabled: true

  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

Expose the health endpoint:

management:
  endpoints:
    web:
      exposure:
        include:
          - health
          - info
          - metrics

The resulting endpoints are:

/actuator/health
/actuator/health/liveness
/actuator/health/readiness

4. The Important Difference Between Health and Readiness

Consider:

/actuator/health

This can represent the overall health of the application and may include health indicators such as:

Database
Redis
Disk Space
Messaging
Other dependencies

But Kubernetes readiness should be deliberately designed.

For example:

Readiness
   |
   +-- Database
   +-- Redis
   +-- Solace
   +-- Certificates
   +-- Initial Configuration
   +-- Cache Warm-up

Only the dependencies that are genuinely required for the application to serve traffic should determine readiness.


5. Example Enterprise Application

Consider a Spring Boot microservice with:

                 Spring Boot Service
                         |
        +----------------+----------------+
        |                |                |
      Oracle           Redis           Solace
        |
   Certificates

At application startup:

Pod Started
     |
     v
Spring Boot Started
     |
     v
HTTP Server Available
     |
     v
Load Certificates
     |
     v
Connect Oracle
     |
     v
Connect Redis
     |
     v
Connect Solace
     |
     v
Warm Required Cache
     |
     v
Application Ready

The critical question is:

At which point should Kubernetes start sending traffic?

The answer should be:

After all mandatory initialization has completed successfully.


6. Recommended Readiness State Model

Instead of treating readiness as simply:

UP / DOWN

think about the application as moving through states.

STARTING
    |
    v
CERTIFICATES_LOADING
    |
    v
DATABASE_INITIALIZING
    |
    v
REDIS_INITIALIZING
    |
    v
SOLACE_INITIALIZING
    |
    v
CACHE_WARMING
    |
    v
READY

If a mandatory initialization step fails:

INITIALIZATION
      |
      v
   FAILURE
      |
      v
NOT READY

This gives you a much more meaningful health model.


7. Using Spring Boot’s Readiness State

Spring Boot provides ApplicationAvailability and readiness state support.

The application can transition to:

ReadinessState.ACCEPTING_TRAFFIC

or:

ReadinessState.REFUSING_TRAFFIC

For example:

@Component
public class ApplicationStartupManager {

    private final ApplicationContext applicationContext;

    public ApplicationStartupManager(
            ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public void markReady() {

        AvailabilityChangeEvent.publish(
                applicationContext,
                ReadinessState.ACCEPTING_TRAFFIC);
    }

    public void markNotReady() {

        AvailabilityChangeEvent.publish(
                applicationContext,
                ReadinessState.REFUSING_TRAFFIC);
    }
}

Now your application can explicitly control its readiness state.


8. Coordinating Multiple Dependencies

For an enterprise microservice, we might maintain:

@Component
public class ApplicationReadinessState {

    private final AtomicBoolean databaseReady =
            new AtomicBoolean(false);

    private final AtomicBoolean redisReady =
            new AtomicBoolean(false);

    private final AtomicBoolean solaceReady =
            new AtomicBoolean(false);

    private final AtomicBoolean certificatesReady =
            new AtomicBoolean(false);

    public boolean isReady() {

        return databaseReady.get()
                && redisReady.get()
                && solaceReady.get()
                && certificatesReady.get();
    }
}

Each initialization component updates its own state.

For example:

databaseReady.set(true);

when database initialization succeeds.

Redis:

redisReady.set(true);

Solace:

solaceReady.set(true);

Certificates:

certificatesReady.set(true);

The overall application becomes ready only when:

Database       = READY
Redis          = READY
Solace         = READY
Certificates   = READY
                  |
                  v
             Application
                READY

9. Custom HealthIndicator

Another approach is to expose this application state through a custom Spring Boot HealthIndicator.

@Component
public class ApplicationReadinessHealthIndicator
        implements HealthIndicator {

    private final ApplicationReadinessState state;

    public ApplicationReadinessHealthIndicator(
            ApplicationReadinessState state) {
        this.state = state;
    }

    @Override
    public Health health() {

        if (state.isReady()) {
            return Health.up()
                    .withDetail("application", "READY")
                    .build();
        }

        return Health.outOfService()
                .withDetail("application", "NOT_READY")
                .build();
    }
}

The resulting health information could look like:

{
  "status": "UP",
  "components": {
    "applicationReadiness": {
      "status": "UP",
      "details": {
        "application": "READY"
      }
    }
  }
}

10. Make the Readiness Information More Detailed

For production troubleshooting, it is useful to expose the individual states.

@Override
public Health health() {

    Health.Builder builder =
            state.isReady()
                    ? Health.up()
                    : Health.outOfService();

    return builder
            .withDetail("database",
                    state.isDatabaseReady())
            .withDetail("redis",
                    state.isRedisReady())
            .withDetail("solace",
                    state.isSolaceReady())
            .withDetail("certificates",
                    state.isCertificatesReady())
            .build();
}

Example:

{
  "status": "OUT_OF_SERVICE",
  "components": {
    "applicationReadiness": {
      "status": "OUT_OF_SERVICE",
      "details": {
        "database": true,
        "redis": true,
        "solace": false,
        "certificates": true
      }
    }
  }
}

This immediately tells an engineer:

The application is not ready because Solace is unavailable.


11. Do Not Perform Expensive Checks on Every Probe Request

This is an important production consideration.

Avoid implementing readiness like:

GET /actuator/health/readiness

        |
        +--> SELECT 1 from Oracle
        |
        +--> Redis PING
        |
        +--> Solace connection test
        |
        +--> Certificate validation

every time Kubernetes calls the endpoint.

If Kubernetes checks every few seconds and you have many pods, this creates unnecessary dependency traffic.

Instead:

Dependency Monitoring
        |
        v
Application State
        |
        v
Readiness Endpoint

The readiness endpoint should ideally be very fast.


12. Event-Driven Dependency State

For some dependencies, you don’t even need periodic polling.

For example, Solace clients can expose connection/disconnection events.

You can update:

solaceReady.set(true);

when connected.

When disconnected:

solaceReady.set(false);

Similarly, Redis and database connectivity can be monitored using appropriate connection-pool or application-level health mechanisms.

This gives you:

Solace Connected
       |
       v
solaceReady = true

Solace Disconnected
       |
       v
solaceReady = false

13. What Happens When Redis Goes Down?

This is where architecture matters.

Suppose:

Application
     |
     +---- Oracle
     |
     +---- Redis

Redis goes down.

Should readiness become DOWN?

Scenario A – Redis is mandatory

If the application cannot process requests without Redis:

Redis DOWN
    |
    v
Readiness DOWN
    |
    v
OpenShift removes pod from Service

This is appropriate.

Scenario B – Redis is only a cache

If the application can fall back to Oracle:

Redis DOWN
    |
    v
Read from Oracle
    |
    v
Application continues

In this case, keeping readiness UP may be the better design.

This is an important architectural decision.


14. What Happens When Solace Goes Down?

Consider a service that receives messages from Solace.

If it cannot consume messages:

Solace DOWN
     |
     v
Consumer unavailable

Depending on the application’s purpose, you might mark readiness as DOWN.

But consider a service that only publishes messages and can buffer/retry them.

Solace being temporarily unavailable may not require removing the pod from HTTP traffic.

Therefore:

Readiness should represent business capability, not simply dependency availability.

This is one of the most important principles in designing production health checks.


15. Certificates and Readiness

Suppose certificates are loaded after the HTTP server starts.

Tomcat Started
     |
     v
Certificate Loading
     |
     v
Certificate Validation

If the application requires those certificates to call downstream systems, it should remain:

NOT READY

until certificate loading succeeds.

For example:

certificatesReady.set(true);

only after:

  • Certificate exists.
  • Certificate is readable.
  • Certificate is valid.
  • Required private key is available.
  • Required trust chain is available.

16. OpenShift Configuration

Your Deployment configuration could look like:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3

The exact values should be tuned based on actual startup and recovery characteristics rather than copied blindly.


17. Consider a Startup Probe

If your application takes a long time to initialize, a startup probe can be useful.

For example:

startupProbe:
  httpGet:
    path: /actuator/health
    port: 8080
  periodSeconds: 10
  failureThreshold: 30

This gives the application up to approximately:

30 × 10 seconds = 300 seconds

to start.

During startup probing, Kubernetes doesn’t prematurely apply liveness/readiness behavior in the same way it would once startup has succeeded.


18. Recommended Three-Probe Model

For a production Spring Boot application:

                  Kubernetes
                      |
          +-----------+-----------+
          |           |           |
       Startup     Liveness    Readiness
          |           |           |
       "Started?"   "Alive?"    "Ready?"
          |           |           |
          v           v           v
       Startup     JVM/App      Business
       process      health      capability

Example:

startupProbe:
  httpGet:
    path: /actuator/health
    port: 8080

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080

19. Recommended Spring Boot Configuration

A reasonable baseline is:

management:
  endpoints:
    web:
      exposure:
        include:
          - health
          - info
          - metrics

  endpoint:
    health:
      probes:
        enabled: true

  health:
    livenessstate:
      enabled: true

    readinessstate:
      enabled: true

Then configure OpenShift to use:

/actuator/health/liveness

and:

/actuator/health/readiness

20. Recommended Architecture for Your Microservice

For an application with:

  • Oracle
  • Redis
  • Solace
  • Certificates
  • Initial configuration
  • Cache initialization

I would use this architecture:

                    Spring Boot
                         |
                ApplicationStateManager
                         |
       +-----------------+------------------+
       |          |          |              |
       v          v          v              v
   Database     Redis      Solace      Certificates
       |          |          |              |
       +----------+----------+--------------+
                         |
                         v
                   Readiness State
                         |
              +----------+----------+
              |                     |
        ACCEPTING_TRAFFIC    REFUSING_TRAFFIC
              |                     |
              v                     v
        OpenShift sends       OpenShift removes
          traffic               pod from Service

The key principle is:

Kubernetes should determine whether the pod receives traffic based on the application’s ability to perform its required business function—not merely whether the JVM process is alive.


21. Readiness State Matrix

A useful way to document your production behavior is:

DatabaseRedisSolaceCertificatesReadiness
UPUPUPUPREADY
DOWNUPUPUPNOT READY
UPDOWNUPUPDepends on Redis being mandatory
UPUPDOWNUPDepends on Solace being mandatory
UPUPUPDOWNNOT READY
UPUPUPLoadingNOT READY
UPUPUPUP + Cache warmingNOT READY
UPUPUPUP + Initialization completeREADY

This matrix should be agreed upon with the application/business team rather than being decided purely from an infrastructure perspective.


22. Important Design Rule: Liveness Should Be Simpler Than Readiness

A common mistake is:

Liveness
   |
   +-- Oracle
   +-- Redis
   +-- Solace
   +-- External APIs
   +-- Certificates

This is dangerous.

Imagine Oracle goes down.

If liveness fails:

Oracle DOWN
    |
    v
Liveness DOWN
    |
    v
OpenShift restarts pod
    |
    v
Pod starts
    |
    v
Oracle still DOWN
    |
    v
Restart again

You can end up with a restart loop.

Instead:

Liveness
   |
   +-- JVM/Application process

Readiness
   |
   +-- Database
   +-- Redis
   +-- Solace
   +-- Certificates
   +-- Required initialization

This allows the pod to remain alive while OpenShift simply stops routing traffic to it.


23. Production Troubleshooting Becomes Much Easier

With a properly designed readiness mechanism, OpenShift can show:

Pod
 |
 +-- Running
 |
 +-- Ready: False

You can then inspect:

/actuator/health/readiness

and discover:

Database       UP
Redis          UP
Solace         DOWN
Certificates   UP

Instead of seeing:

Pod Restarting

with no obvious reason.

That distinction can significantly reduce troubleshooting time during production incidents.


24. Final Recommended Pattern

For enterprise Spring Boot microservices running on Kubernetes/OpenShift:

Liveness

Use:

/actuator/health/liveness

Keep it lightweight.

Readiness

Use:

/actuator/health/readiness

and make it represent the actual business readiness of the application.

Startup

Use:

startupProbe

when startup is long or unpredictable.

Dependency State

Track critical dependencies:

Oracle
Redis
Solace
Certificates
Required configuration
Cache initialization

Avoid

Do not use:

/actuator/info

as a readiness or liveness endpoint.

Do not perform expensive dependency calls on every Kubernetes probe.

Do not make every dependency automatically mandatory for readiness.

Do not make liveness dependent on external infrastructure.


Conclusion

A Kubernetes health check should not simply answer:

“Is the Spring Boot process running?”

A production-ready microservice should answer three different questions:

Startup
   ↓
"Have I finished initialization?"

Liveness
   ↓
"Am I alive and capable of continuing?"

Readiness
   ↓
"Can I safely receive and process production traffic?"

For an enterprise Spring Boot application using Oracle, Redis, Solace and certificates, the most robust design is to maintain an application-level readiness state, update it as critical initialization and dependency states change, and expose that state through Spring Boot’s Kubernetes readiness endpoint.

This gives Kubernetes/OpenShift a reliable signal and gives engineers a clear explanation of why a pod is not ready, rather than simply reporting that something failed.

Leave a Reply

Your email address will not be published. Required fields are marked *