HealthCheck, HealthCheckRegistry, and aggregation logic for combining multiple dependency checks into one overall status, mapped directly onto Spring Boot Actuator's own HealthIndicator design.
Published September 23, 2026
interface HealthCheck { HealthStatus check(); String name(); }
enum HealthStatus { UP, DOWN, DEGRADED }
class HealthCheckRegistry {
List<HealthCheck> checks = new CopyOnWriteArrayList<>();
void register(HealthCheck check) { checks.add(check); }
}
HealthCheck as an interface (not a fixed enum of known checks) is what makes the registry OPEN for new dependency checks without ever modifying the registry itself — adding a check for a new dependency (a message broker connection, an external API) is purely additive, the same Open/Closed pattern seen throughout this Machine Coding chapter.
class HealthAggregator {
HealthStatus aggregate(List<HealthCheck> checks) {
List<HealthStatus> results = checks.stream().map(HealthCheck::check).toList();
if (results.stream().anyMatch(s -> s == HealthStatus.DOWN)) return HealthStatus.DOWN;
if (results.stream().anyMatch(s -> s == HealthStatus.DEGRADED)) return HealthStatus.DEGRADED;
return HealthStatus.UP;
}
}
The aggregation LOGIC itself is a real design decision, not a trivial detail: should ANY single DOWN dependency make the WHOLE service report DOWN, or only checks marked as "essential"? This directly mirrors Health Checks' essential-vs-non-essential distinction — a more sophisticated version would tag each HealthCheck with a criticality level, and only ESSENTIAL checks failing should drag the overall status to DOWN, while non-essential checks failing might only produce DEGRADED (still serving traffic, with reduced functionality).
// Actuator's actual interface — structurally identical to the HealthCheck above
interface HealthIndicator { Health health(); }
This exercise's HealthCheck/HealthCheckRegistry/aggregation design ISN'T a hypothetical toy — it's essentially a from-scratch reimplementation of what Spring Boot Actuator already provides via HealthIndicator (covered concretely in Health Checks). Building it yourself here is what makes Actuator's own design legible — recognizing the SAME Open/Closed extension point, the SAME aggregation-across-multiple-checks pattern, in a framework you already use daily.
Q: How would you add per-check timeout handling, so one slow HealthCheck doesn't block the whole aggregation? A: Wrap each individual check's execution with a timeout (Timeout Strategy) — running checks in parallel (each with its own bounded timeout) rather than sequentially, treating a check that times out as DOWN (or DEGRADED) rather than letting it stall the entire aggregation indefinitely.
Q: Should the aggregator cache results rather than re-running every check on every request? A: Often yes, for exactly the reason discussed in Health Checks' follow-up questions — running an expensive check (a real database query) on every single health-check poll adds avoidable load; caching results for a short interval and only re-running periodically is a common, practical optimization.
Q: How does 'criticality level' per check actually get decided? A: This is a genuine per-dependency design decision, not something derivable automatically — it requires explicitly reasoning about each dependency's role (per Health Checks' essential-vs-non-essential framing), and different services legitimately reach different answers for what looks like a superficially similar dependency.
Q: Does this design need to distinguish liveness-check aggregation from readiness-check aggregation? A: Yes — mirroring Health Checks' liveness/readiness split, a real implementation would maintain TWO separate check sets/aggregations (a minimal liveness set with no external dependencies, a broader readiness set including real dependency checks), not one single aggregated status serving both purposes.