skip to content

How do @ReadOperation, @WriteOperation, and @DeleteOperation map to HTTP, and how are their method parameters bound?

level: middleimportance: must knowfreq 50%

answer

  1. Read=GET, Write=POST, Delete=DELETE
  2. @Selector = path; else query (read) or JSON body (write)
  3. ApplicationConversionService for types
  4. needs -parameters for names
  5. 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 s

The 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
java
@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

for a junior

Know the three verbs map to GET/POST/DELETE.

for a middle

Explain the read=query vs write=body binding split and the -parameters requirement.

for a senior

Add conversion service, optionality of non-selector params, and void/null status semantics.

for a principal

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

context