How do HealthContributor and CompositeHealthContributor let you group and nest health checks?
answer
- HealthContributor = marker; HealthIndicator = leaf; Composite = branch
- CompositeHealthContributor.fromMap(name->contributor)
- nested components tree, worst status bubbles up
- HealthContributorRegistry auto-registers by bean name
- groups select contributors by name for liveness/readiness
basics
~20 sHealthContributor is the marker interface that HealthIndicator implements. A CompositeHealthContributor bundles several named contributors under one parent, so the endpoint shows a nested tree — for example one "externalApis" node with a child per API.
solid answer
~40 sIn Actuator's model, HealthContributor is the base marker interface; a single check is a HealthIndicator (which extends HealthContributor), and a group is a CompositeHealthContributor, which exposes named child contributors and can nest arbitrarily. Actuator's HealthContributorRegistry holds all top-level contributors; when you hit /actuator/health it walks the tree, and each composite renders as a nested "components" block keyed by child name. You create a composite with CompositeHealthContributor.fromMap(map), letting you register, say, one contributor per downstream service under a single parent key. The reactive equivalent is ReactiveHealthContributor / CompositeReactiveHealthContributor. Aggregation still bubbles up the worst child status to the parent and ultimately the endpoint. Composition is how you keep many related checks organised and how health groups (management.endpoint.health.group) select subsets by name for readiness vs liveness probes.
code
java · 24 linesimport org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class DownstreamHealthConfig {
// One composite named "downstream" grouping a child per external service.
@Bean
HealthContributor downstream(java.util.List<ServiceClient> clients) {
Map<String, HealthContributor> children = new LinkedHashMap<>();
for (ServiceClient c : clients) {
children.put(c.name(), () -> { // HealthIndicator is a functional interface
return c.isReachable()
? org.springframework.boot.actuate.health.Health.up().build()
: org.springframework.boot.actuate.health.Health.down().build();
});
}
return CompositeHealthContributor.fromMap(children);
}
}go deeper
Recognise that HealthIndicator is one check and that checks can be grouped, showing a nested tree.
Explain the HealthContributor/HealthIndicator/CompositeHealthContributor hierarchy and CompositeHealthContributor.fromMap.
Discuss the registry, worst-status bubbling, dynamic composites, and how naming feeds health groups.
Design contributor topology aligned to operational domains and readiness/liveness groups; manage runtime registration and probe noise across services.
## The type hierarchy Actuator models health as a **tree of contributors**: - **`HealthContributor`** — the root **marker interface**. It carries no methods itself; it just says "I contribute to health." - **`HealthIndicator`** — a leaf. It `extends HealthContributor` and adds `Health health()`. One indicator = one check. - **`CompositeHealthContributor`** — a branch. It groups **named** child `HealthContributor`s and is itself a `HealthContributor`, so composites can nest to any depth. The reactive mirror is `ReactiveHealthContributor`, `ReactiveHealthIndicator`, and `CompositeReactiveHealthContributor`. ## Why the split exists Older Boot used `HealthIndicator` + `CompositeHealthIndicator` returning a single `Health`. Boot 2.2 introduced the `HealthContributor` hierarchy to (a) support nesting cleanly and (b) share one model between blocking and reactive worlds. `HealthAggregator`-style single-object aggregation gave way to a **tree** rendered as nested `components`. ## Building a composite The simplest factory is `CompositeHealthContributor.fromMap(Map<String, ? extends HealthContributor>)`: ```java @Bean HealthContributor externalApis(PaymentHealthIndicator pay, ShippingHealthIndicator ship) { return CompositeHealthContributor.fromMap(Map.of( "payment", pay, "shipping", ship)); } ``` The bean name (`externalApis`) becomes the parent key; each map entry becomes a child key: ```json { "status": "UP", "components": { "externalApis": { "status": "UP", "components": { "payment": { "status": "UP" }, "shipping": { "status": "DOWN" } } } } } ``` You can also implement `CompositeHealthContributor` yourself (it's `Iterable<NamedContributor<HealthContributor>>` with `getContributor(String)`), e.g. to build children dynamically from a service registry. ## The registry Actuator keeps top-level contributors in a **`HealthContributorRegistry`** (reactive: `ReactiveHealthContributorRegistry`). Every `HealthContributor` bean is auto-registered under its de-suffixed bean name. You can inject the registry to register/unregister contributors at runtime. ## Aggregation through the tree Each composite's status is the aggregate (via `StatusAggregator`) of its children — worst wins — and that bubbles up to the endpoint's top-level status. So a single DOWN leaf, however deeply nested, can turn the whole endpoint DOWN and yield HTTP 503. ## Relationship to health groups **Health groups** (`management.endpoint.health.group.<name>.include/exclude`) select a **subset of contributors by name** and expose them at `/actuator/health/<name>`. Kubernetes support builds `liveness` and `readiness` groups from `LivenessStateHealthIndicator` and `ReadinessStateHealthIndicator`. Composition and naming are what make group include/exclude lists meaningful. ## Gotchas - **Naming collisions:** child keys must be unique within a composite; duplicate keys in the map throw. - **Show-components:** the nested breakdown obeys `management.endpoint.health.show-components`/`show-details` — it can be hidden. - **Don't over-nest:** deep trees are noisy for probes; group by operational meaning (readiness-critical vs informational). - **Blocking children under reactive apps** are adapted individually; a composite doesn't change that. ## When to use Reach for a composite when you have many related checks (one per downstream, per shard, per tenant) that you want grouped under one operational heading and toggled together in a health group.
- How do health groups relate to contributor composition?Groups (management.endpoint.health.group.<name>.include) select contributors by their registered names and expose them at /actuator/health/<name>. Meaningful names from composition/registration are what group include/exclude lists reference — e.g. Kubernetes readiness/liveness groups.
- If a deeply nested child in a composite is DOWN, what is the top-level endpoint status?DOWN. Aggregation bubbles the worst status up through each composite to the endpoint, so any DOWN leaf makes the whole endpoint DOWN (HTTP 503 by default).
saying these in an interview costs you the question
- Saying HealthContributor has a health() method (it's a marker; HealthIndicator adds health())
- Claiming a composite averages or votes on child statuses (it takes the worst)
- Confusing composites with health groups — composites nest contributors, groups select subsets by name for a URL
- Thinking nesting changes the reactive-vs-blocking adaptation of individual leaves