How do you create a basic custom Actuator endpoint in Spring Boot, and what must you do before it becomes reachable over HTTP?
answer
- @Endpoint(id=...) + @ReadOperation
- enabled != exposed
- exposure.include to reach over web
- path = /actuator + id
- id must be ^[a-z][a-z0-9]*$
basics
~10 sMake a Spring bean annotated with @Endpoint(id="...") and give it a method annotated @ReadOperation. Then expose it via management.endpoints.web.exposure.include so it appears under /actuator/<id>.
solid answer
~40 sYou annotate a Spring-managed bean (e.g. @Component) with @Endpoint(id="features"). Inside it, a method with @ReadOperation handles GET, @WriteOperation handles POST, @DeleteOperation handles DELETE. The endpoint's HTTP path is management.endpoints.web.base-path (default /actuator) + the id, so /actuator/features. Two things gate reachability: the endpoint must be enabled (endpoints are enabled by default except shutdown), and it must be exposed — only health is web-exposed by default, so you add its id to management.endpoints.web.exposure.include (or set it to *). @Endpoint is technology-agnostic: the same bean is published over both HTTP (web) and JMX. The id must be lowercase alphanumeric matching ^[a-z][a-z0-9]*$ (no hyphens or camelCase).
code
java · 18 linesimport org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Endpoint(id = "features") // -> GET /actuator/features (once exposed)
public class FeaturesEndpoint {
private final Map<String, Boolean> flags = Map.of("beta-ui", true, "new-search", false);
@ReadOperation
public Map<String, Boolean> features() {
return flags; // serialized to JSON
}
}
// application.yml:
// management.endpoints.web.exposure.include: health,featuresgo deeper
Know @Endpoint + @ReadOperation and that you must add the id to exposure.include; path is /actuator/<id>.
Explain enabled-vs-exposed as two independent gates and the id naming constraint.
Contrast @Endpoint (both technologies) with @WebEndpoint/@JmxEndpoint and note null/void return semantics.
Frame endpoints vs @RestController: when the uniform Actuator exposure/security/JMX model is worth it versus a plain controller.
## What an Actuator endpoint is Spring Boot Actuator turns operational concerns (health, metrics, env, etc.) into **endpoints** — small units of monitoring/management functionality. A *custom* endpoint lets you publish your own operational data or actions the same way the built-ins do. ## Creating one Create a bean and annotate it with `@org.springframework.boot.actuate.endpoint.annotation.@Endpoint(id = "features")`. The bean must be discoverable by Spring (annotate with `@Component`, or declare it with `@Bean` in a `@Configuration`). Inside, add an operation method: - `@ReadOperation` → exposed as HTTP **GET** - `@WriteOperation` → HTTP **POST** - `@DeleteOperation` → HTTP **DELETE** ## The id and the URL `id` is the endpoint's technology-agnostic name. Over HTTP the path is `management.endpoints.web.base-path` (default `/actuator`) concatenated with the id — so id `features` → `/actuator/features`. Since Spring Boot 2.x the id **must** match `^[a-z][a-z0-9]*$`: lowercase, starts with a letter, no hyphens, no camelCase (those were deprecated/removed). `id = "my-endpoint"` fails startup. ## Two independent gates: enabled vs exposed A common gotcha is conflating these. 1. **Enabled** — whether the endpoint bean is created at all. Endpoints are enabled by default (except `shutdown`). Toggle with `management.endpoint.<id>.enabled=false` or globally `management.endpoints.enabled-by-default=false`. 2. **Exposed** — whether an *enabled* endpoint is reachable over a given technology. For **web**, only `health` is exposed by default. You must add your id: `management.endpoints.web.exposure.include=features,health` (or `*` for all — avoid `*` in production). There is a matching `.exclude`. JMX has its own `management.endpoints.jmx.exposure.*` (and JMX is disabled by default since Boot 2.2). An endpoint can be enabled but not exposed (created, but 404 over HTTP), or exposed but disabled (never created). ## Technology-agnostic by default `@Endpoint` publishes the bean over **both** HTTP and JMX. If you only want one technology, use `@WebEndpoint` (HTTP only) or `@JmxEndpoint` (JMX only). ## Return values The return object is serialized to JSON (via the Actuator's Jackson-based message conversion). Returning `void` yields HTTP 204 No Content; returning `null` from a read operation yields 404 Not Found. ## When to use Use a custom endpoint for operational read/act surfaces — a feature-flag dump, a cache-clear action, a queue-depth probe — that fit Actuator's uniform exposure/security model. For general application APIs, use a regular `@RestController` instead; endpoints are for management, are grouped under `/actuator`, and share Actuator security/exposure config.
- You added @Endpoint and the bean is created, but GET /actuator/features returns 404. Why?It's enabled but not exposed over web. Only health is web-exposed by default. Add the id to management.endpoints.web.exposure.include.
- What's the difference between disabling and not exposing an endpoint?Disabling (management.endpoint.<id>.enabled=false) prevents the endpoint bean from being created at all. Not exposing keeps the bean but makes it unreachable over that technology (web/JMX). They're independent gates.
saying these in an interview costs you the question
- Thinking @Endpoint alone makes it reachable — forgetting exposure.include
- Using hyphens/camelCase in the id (must be lowercase alphanumeric)
- Assuming * exposure is fine in production
- Confusing enabled with exposed