How do @ReadOperation, @WriteOperation, and @DeleteOperation map to HTTP, and how are their method parameters bound?
answer
- Read=GET, Write=POST, Delete=DELETE
- @Selector = path; else query (read) or JSON body (write)
- ApplicationConversionService for types
- needs -parameters for names
- void->204, null read->404
basics
~10 s@ReadOperation=GET, @WriteOperation=POST, @DeleteOperation=DELETE. Path parameters use @Selector. For a read/delete, other params come from query string; for a write, they come from the JSON request body.
solid answer
~40 sThe three operation annotations map to HTTP verbs: @ReadOperation→GET, @WriteOperation→POST, @DeleteOperation→DELETE. Parameter binding depends on role and verb. A @Selector parameter is taken from the URL path (e.g. /actuator/features/{name}). Non-selector parameters on a @ReadOperation/@DeleteOperation are read from query parameters; on a @WriteOperation they're read from the JSON request body, matched by key to parameter name. Values are converted via Spring's ApplicationConversionService, so types like enums, Integer, Duration work. Non-selector params default to optional/nullable unless you require them. Return values serialize to JSON; void→204, null from a read→404. For a write, returning void gives 204. Because Actuator has no @RequestParam-style annotations, parameter names must be preserved at compile time (-parameters flag, which Spring Boot's Gradle/Maven plugins enable).
code
java · 23 lines@Component
@Endpoint(id = "features")
public class FeaturesEndpoint {
private final Map<String, Boolean> flags = new ConcurrentHashMap<>();
// GET /actuator/features/{name} (name is a path selector)
@ReadOperation
public Boolean feature(@Selector String name) {
return flags.get(name); // null -> HTTP 404
}
// POST /actuator/features/{name} body: {"enabled": true}
@WriteOperation
public void configure(@Selector String name, boolean enabled) {
flags.put(name, enabled); // void -> HTTP 204
}
// DELETE /actuator/features/{name}
@DeleteOperation
public void remove(@Selector String name) {
flags.remove(name);
}
}go deeper
Know the three verbs map to GET/POST/DELETE.
Explain the read=query vs write=body binding split and the -parameters requirement.
Add conversion service, optionality of non-selector params, and void/null status semantics.
Discuss why the operation model is intentionally minimal (no PUT/PATCH, flat body) and its implications for API design.
## The verb mapping Inside an `@Endpoint` bean, each operation method is annotated with exactly one of: | Annotation | HTTP verb | Typical use | |---|---|---| | `@ReadOperation` | GET | Fetch state (idempotent, no body) | | `@WriteOperation` | POST | Create/update/trigger with a JSON body | | `@DeleteOperation` | DELETE | Remove/reset a resource | There is no PUT/PATCH mapping — Actuator's operation model is deliberately small. ## Two kinds of parameters **1. Selectors (`@Selector`)** bind to *path* segments. `@ReadOperation public X get(@Selector String name)` on endpoint id `features` maps to `GET /actuator/features/{name}`. **2. Non-selector parameters** bind by *name*: - On `@ReadOperation`/`@DeleteOperation` (no HTTP body) → from **query parameters**. `GET /actuator/foo?limit=10` binds `limit`. - On `@WriteOperation` (has a body) → from the **JSON request body**, top-level keys matched to parameter names. POST body `{"enabled": true}` binds `boolean enabled`. The body is a flat JSON object of simple values; Actuator write operations are not designed to bind arbitrarily nested DTOs the way MVC `@RequestBody` does. ## Type conversion Bound values are converted using `ApplicationConversionService`, so `Integer`, `Long`, `Boolean`, enums, `Duration`, etc. convert from their string/JSON forms. A conversion failure yields HTTP 400. ## Parameter names must survive compilation Actuator has no `@RequestParam("limit")`-style name annotation for non-selectors — it relies on the **actual parameter name**. That name is only present in the bytecode if compiled with `-parameters` (`javac`) / `-java-parameters` (Kotlin). Spring Boot's Maven and Gradle plugins enable this by default, but a hand-rolled build that strips it will make parameters fail to bind. This is a classic gotcha. ## Optionality Non-selector parameters are **optional by default** (absent → null). To require one, annotate with `@org.springframework.boot.actuate.endpoint.annotation.Nullable`? No — the reverse: use `@javax`/nothing for optional; to make it mandatory you validate manually, or on web the framework treats missing required primitives as a 400. In practice: object-typed params are optional (null when absent); primitive params are effectively required. ## Return semantics - Return a POJO/Map → serialized to JSON (Actuator media type). - Return `void` → **204 No Content**. - Return `null` from a `@ReadOperation` → **404 Not Found** (useful with a selector for a missing resource). - Wrap in `WebEndpointResponse<T>` to set a custom status code. ## Gotchas summary - Query-vs-body binding flips between read and write — passing a write's arguments as query params silently leaves them null. - Missing `-parameters` breaks non-selector binding. - No PUT/PATCH; model updates as POST (`@WriteOperation`).
- Your @WriteOperation parameter is always null even though you send ?enabled=true. Why?Write operations read non-selector params from the JSON request body, not the query string. Send it as POST body {"enabled": true}, or the value stays null.
- Why might non-selector parameter binding fail with an odd 'arg0' name?The code was compiled without -parameters, so real parameter names were dropped. Actuator has no @RequestParam name annotation for non-selectors, so it can't match. Spring Boot's build plugins normally enable -parameters.
saying these in an interview costs you the question
- Claiming @WriteOperation reads query parameters
- Expecting PUT/PATCH support
- Using @RequestParam/@RequestBody inside an endpoint (wrong programming model)
- Thinking parameter names always survive without -parameters