Definition
- Actuator – a Spring Boot module that exposes operational HTTP endpoints for monitoring and management without writing custom code.
- /health – reports application health (UP/DOWN) and is used by Kubernetes for liveness and readiness probes.
- /prometheus – exports metrics in Prometheus format (counters, gauges, histograms) for scraping and Grafana visualization.
- /loggers – allows runtime log level changes without restarting the application, critical for production incident debugging.
- Custom HealthIndicator – a Spring bean that adds business-specific health checks (database connectivity, external API availability) to the /health response.
- Management port – runs Actuator endpoints on a separate port so they are accessible only within the cluster, not exposed to the public internet.
Think of the control room in a National Bank of India branch. The health monitor shows at a glance if the vault is open, if the network is connected, and if the cash counter is staffed (health checks). The transaction counter on the wall shows how many transactions per second are happening (metrics). The CCTV recording detail level can be increased during an incident and decreased afterward (runtime log level changes). The control room has its own entrance that only staff can access, separate from the customer entrance (management port).
Understanding the Concepts
What is Spring Boot Actuator?
Spring Boot Actuator is a module that adds production-ready monitoring and management endpoints to your application. You add a single dependency, and Spring Boot auto-configures endpoints for health checks, metrics, environment inspection, bean listing, thread dumps, and log management. These endpoints are essential for running applications in production, especially in Kubernetes.
The key endpoints are:
/actuator/health: reports whether the application is UP or DOWN./actuator/health/liveness: Kubernetes liveness probe./actuator/health/readiness: Kubernetes readiness probe./actuator/prometheus: exports all metrics in Prometheus format./actuator/loggers: view and change log levels at runtime./actuator/info: application information (version, git commit, etc.)./actuator/env: shows all configuration properties and their sources./actuator/beans: lists all beans in the ApplicationContext./actuator/threaddump: captures a thread dump for debugging deadlocks.
<!-- pom.xml: add Actuator dependency --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-actuatorsyn-kw"></artifactIdsyn-kw">>
syn-kw"></dependencysyn-kw">>
<!-- For Prometheus metrics export --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>io.micrometersyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>micrometer-registry-prometheussyn-kw"></artifactIdsyn-kw">>
syn-kw"></dependencysyn-kw">>
# application.yml: configure Actuator
management:
endpoints:
web:
exposure:
include: health, prometheus, loggers, info, env
endpoint:
health:
show-details: when-authorized
health:
probes:
enabled: true
GET /actuator/health
{
"status": "UP",
"components": {
"db": { "status": "UP" },
"diskSpace": { "status": "UP" },
"coreBanking": { "status": "UP", "details": { "provider": "reachable" } }
}
}
How do you add a custom health check?
A custom HealthIndicator checks business-specific dependencies that Spring Boot does not know about. At National Bank of India, Rajesh needs to check whether the core banking system is reachable. If it is down, the service should report itself as not ready to serve traffic.
You implement the HealthIndicator interface and return Health.up() or Health.down() with descriptive details. Spring Boot automatically includes your custom indicator in the /actuator/health response.
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() {
try {
boolean isReachable = coreBankingClient.ping();
if (isReachable) {
return Health.up()
.withDetail("provider", "reachable")
.withDetail("responseTime", "45ms")
.build();
}
return Health.down()
.withDetail("provider", "unreachable")
.withDetail("reason", "connection refused")
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
GET /actuator/health
{
"status": "DOWN",
"components": {
"coreBanking": {
"status": "DOWN",
"details": {
"provider": "unreachable",
"reason": "connection refused"
}
},
"db": { "status": "UP" }
}
}
What is the difference between liveness and readiness probes?
Kubernetes uses two types of probes to manage pods, and confusing them is a common production mistake. Liveness checks if the JVM is alive and functioning. Readiness checks if the application is ready to accept traffic.
| Aspect | Liveness Probe | Readiness Probe |
|---|---|---|
| Purpose | Is the JVM alive? | Can the app serve requests? |
| Failure action | Kubernetes restarts the pod | Kubernetes removes pod from load balancer |
| What to check | Internal JVM health only | DB, cache, external services, startup tasks |
| External deps | Never include | Always include |
| Endpoint | /actuator/health/liveness | /actuator/health/readiness |
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 create a cascading failure.
# Kubernetes deployment
spec:
containers:
- name: account-service
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 9092
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 9092
initialDelaySeconds: 15
periodSeconds: 10
How do you create custom business metrics?
Micrometer is the metrics facade that integrates with Prometheus. You define custom metrics using Counter (things that only go up, like total transactions), Gauge (values that go up and down, like active connections), and Timer (duration measurements, like request processing time).
package com.bank.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class TransferService {
private final Counter successCounter;
private final Counter failureCounter;
private final Timer transferTimer;
public TransferService(MeterRegistry registry) {
this.successCounter = Counter.builder("bank.transfers.success")
.description("Total successful transfers")
.tag("type", "NEFT")
.register(registry);
this.failureCounter = Counter.builder("bank.transfers.failure")
.description("Total failed transfers")
.tag("type", "NEFT")
.register(registry);
this.transferTimer = Timer.builder("bank.transfers.duration")
.description("Transfer processing time")
.register(registry);
}
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
transferTimer.record(() -> {
try {
executeTransfer(fromAccount, toAccount, amount);
successCounter.increment();
} catch (Exception e) {
failureCounter.increment();
throw e;
}
});
}
}
GET /actuator/prometheus
# HELP bank_transfers_success_total Total successful transfers
# TYPE bank_transfers_success_total counter
bank_transfers_success_total{type="NEFT"} 1542.0
# HELP bank_transfers_failure_total Total failed transfers
# TYPE bank_transfers_failure_total counter
bank_transfers_failure_total{type="NEFT"} 3.0
# HELP bank_transfers_duration_seconds Transfer processing time
# TYPE bank_transfers_duration_seconds summary
bank_transfers_duration_seconds_count 1545
bank_transfers_duration_seconds_sum 231.75
How do you run Actuator on a separate port?
In production, Actuator endpoints should not be accessible from the public internet. You run them on a separate management port that is only accessible within the Kubernetes cluster. The main application serves customer traffic on port 8080, while Actuator serves monitoring traffic on port 9092.
# application-prod.yml
server:
port: 8080 # public traffic
management:
server:
port: 9092 # internal monitoring only
endpoints:
web:
exposure:
include: health, prometheus, loggers, info
base-path: /actuator
Public load balancer:
http://account-service:8080/api/v1/accounts/SAV2001 <- Priya's request
Internal cluster only:
http://account-service:9092/actuator/health <- K8s probe
http://account-service:9092/actuator/prometheus <- Prometheus scraper
http://account-service:9092/actuator/loggers <- SRE during incident
How do you change log levels at runtime?
The /actuator/loggers endpoint lets you change the log level of any package at runtime without restarting the application. This is critical during production incidents when you need DEBUG-level detail for a specific service without restarting the pod and losing the state of the issue.
# Check current log level for a package
curl http://localhost:9092/actuator/loggers/com.bank.service
# Change to DEBUG for troubleshooting
curl -X POST http://localhost:9092/actuator/loggers/com.bank.service \
-H 'Content-Type: application/json' \
-d '{"configuredLevel": "DEBUG"}'
# Revert to INFO after the incident
curl -X POST http://localhost:9092/actuator/loggers/com.bank.service \
-H 'Content-Type: application/json' \
-d '{"configuredLevel": "INFO"}'
Before change:
{"configuredLevel": "INFO", "effectiveLevel": "INFO"}
After change:
{"configuredLevel": "DEBUG", "effectiveLevel": "DEBUG"}
Now all log.debug() calls in com.bank.service produce output.
What are common mistakes with Actuator?
The most common mistake is exposing all Actuator endpoints to the public internet. Endpoints like /env and /beans reveal sensitive configuration details including database credentials and API keys.
# WRONG: expose everything to the internet
management:
endpoints:
web:
exposure:
include: "*"
# /actuator/env shows database passwords, API keys, secrets
# CORRECT: expose only what is needed, on a separate port
management:
server:
port: 9092 # internal port only
endpoints:
web:
exposure:
include: health, prometheus, loggers, info
endpoint:
health:
show-details: when-authorized # require auth for details
env:
enabled: false # disable entirely
Another mistake is including external dependencies in the liveness probe. If the database is unreachable, the pod should be removed from the load balancer (readiness fails), not restarted (liveness fails). Restarting pods during a database outage creates a restart loop that makes recovery harder.
What should a senior engineer know?
- Use
@Timedannotation from Micrometer to automatically time controller methods without manual Timer code. - Custom
HealthIndicatorbeans are automatically included in the health response. Group them into readiness and liveness usingmanagement.endpoint.health.group.readiness.includeandmanagement.endpoint.health.group.liveness.include. - Spring Boot Admin is a web application that provides a dashboard for monitoring multiple Spring Boot instances. Each application registers with the Admin server, and the dashboard shows health status, metrics, log levels, and thread dumps in a single UI.
- In a microservices architecture, Pankaj configures Prometheus to scrape all services, stores metrics in a time-series database, and visualizes them in Grafana with alerts for SLA breaches.
package com.bank.controller;
import io.micrometer.core.annotation.Timed;
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;
}
@Timed(value = "bank.accounts.getBalance", description = "Time to fetch account balance")
@GetMapping("/{accountId}/balance")
public BigDecimal getBalance(@PathVariable String accountId) {
return accountService.getBalance(accountId);
}
}
What are common interview follow-up questions?
- How do you secure Actuator endpoints? Use Spring Security to require authentication for sensitive endpoints, or rely on network-level security by running Actuator on an internal port that is not exposed through the load balancer.
- Can you create a composite health indicator? Yes. Use
CompositeHealthContributorto group related health checks under a single name. - What metrics does Spring Boot auto-configure? JVM metrics (memory, GC, threads), HTTP server metrics (request count, duration, status codes), connection pool metrics (HikariCP active/idle connections), and cache metrics.
- How does Prometheus scraping work? Prometheus periodically sends GET requests to
/actuator/prometheusand stores the time-series data. Grafana queries Prometheus to render dashboards and trigger alerts.
Visual Overview
Actuator Architecture:
┌────────────────────────────────────────────────┐
│ Spring Boot Application │
│ │
│ Port 8080 (public) Port 9092 (internal) │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ /api/v1/... │ │ /actuator/health │ │
│ │ Customer API │ │ /actuator/prom │ │
│ │ │ │ /actuator/loggers│ │
│ └──────────────┘ └──────────────────┘ │
└────────────────────────────────────────────────┘
│ │
▼ ▼
Load Balancer ┌───────────────┐
(public internet) │ Kubernetes │
│ liveness + │
│ readiness │
└───────┬───────┘
│
┌───────┴───────┐
│ Prometheus │
│ (scrapes │
│ /prometheus)│
└───────┬───────┘
│
┌───────┴───────┐
│ Grafana │
│ (dashboards │
│ + alerts) │
└───────────────┘
Health Probe Decision Flow:
┌──────────────┐
│ K8s checks │
│ /health/ │
│ liveness │──── DOWN ──→ Restart pod
└──────┬───────┘
│ UP
▼
┌──────────────┐
│ K8s checks │
│ /health/ │
│ readiness │──── DOWN ──→ Remove from load balancer
└──────┬───────┘
│ UP
▼
┌──────────────┐
│ Pod receives │
│ traffic │
└──────────────┘
Key Takeaway
Actuator provides production monitoring endpoints that are essential for running Spring Boot applications in Kubernetes. The health endpoint drives liveness and readiness probes: liveness checks if the JVM is alive (never include external dependencies, because a database outage should not trigger pod restarts), and readiness checks if the service can handle traffic (include database, cache, and custom HealthIndicators for business-critical systems like core banking). Running Actuator on a separate management port ensures monitoring endpoints are accessible only within the cluster.
For metrics, Micrometer integrates with Prometheus. Rajesh defines custom business metrics at National Bank of India: a Counter for total NEFT transfers, a separate Counter for failed transfers, and a Timer for processing duration. Prometheus scrapes the /actuator/prometheus endpoint, stores time-series data, and Grafana renders dashboards with alerts when the failure rate exceeds thresholds. The /actuator/loggers endpoint allows changing log levels at runtime during incidents, switching from INFO to DEBUG for a specific package to diagnose the issue, then reverting to INFO afterward, all without restarting the pod.
The critical mistake to avoid is exposing all Actuator endpoints to the public internet. Endpoints like /env and /beans reveal sensitive configuration including database credentials. Expose only what is needed, on an internal port, with authentication for sensitive details.
Remember one sentence: Actuator gives you /health for Kubernetes probes, /prometheus for Grafana dashboards, and /loggers for runtime debugging, all on a separate port that the public internet never sees.
---