> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0007-uniform-reactive-controller-pipeline.md).

# ADR-0007: Uniform reactive controller pipeline

## Status

**Accepted.** Reconstructed from the codebase on 2026-05-30; the decision is live in the source today.

## Context

ODD Platform's API server is built on Spring WebFlux — every endpoint is reactive. A reactive controller method can be shaped many ways: it can return a bare body type, a `Mono<T>`, a `Flux<T>`, or a `Mono<ResponseEntity<T>>`; it can translate its own errors into HTTP status codes, or defer that to a shared handler. Left unconstrained, a large team ends up with a mix of all of these, and a reader cannot predict the shape of an endpoint they have not seen.

A platform this size benefits from one answer: a single response shape every controller follows, and a single place where exceptions become HTTP responses.

## Decision

**Every controller method returns `Mono<ResponseEntity<T>>`, and the success path maps the result with `.map(ResponseEntity::ok)`** (or `.thenReturn(ResponseEntity.noContent().build())` for void / delete results). Controllers do **not** translate their own exceptions — there is no per-controller `@ExceptionHandler`.

**Error translation is centralised in a single `@RestControllerAdvice` class, `ControllerAdvice`.** It maps the platform's exception hierarchy to status codes in one place: `BadUserRequestException` → 400, `NotFoundException` → 404, `UniqueConstraintException` and `CascadeDeleteException` → 400, validation (`WebExchangeBindException`) → 400, `GenAIException` → 500, framework `ResponseStatusException`s → their own embedded status (an unmatched route stays 404, invalid request input stays 400; added in 0.28.0 — before that the catch-all re-branded them 500), and a catch-all `Exception` → 500. Each is rendered into a consistent `ErrorResponse` body carrying a message, an error code, and `resolvable` / `retryable` flags.

A controller method is therefore a pure delegate: take the request, call the service, wrap the result with `ResponseEntity::ok`. Everything about *how an error becomes an HTTP response* lives in one class.

## Consequences

* Every endpoint has a **predictable shape**. A reader who has seen one controller method knows the form of all of them; a contributor writing a new one follows the same `flatMap(service::call).map(ResponseEntity::ok)` pipeline.
* **Error responses are consistent** across the whole surface — the same body schema, status mapping, and resolvable/retryable semantics, because they all come from `ControllerAdvice`. Adding a new exception type means adding one handler there, not editing controllers.
* A controller needing a **non-200 success** (for example a delete returning 204) opts in explicitly with `noContent()` / `thenReturn(...)`; the default is `ResponseEntity::ok`.
* The trade-off: a controller's source does not show how its exceptions map to HTTP — that knowledge lives in `ControllerAdvice`. The uniformity is bought by accepting that error mapping is read in one central place rather than next to each method.

## Evidence

* `odd-platform-api/.../controller/exception/ControllerAdvice.java` — the single `@RestControllerAdvice`: `@ExceptionHandler` methods for `BadUserRequestException` (400), `NotFoundException` (404), `UniqueConstraintException` (400), `CascadeDeleteException` (400), `WebExchangeBindException` (400), `GenAIException` (500), `ResponseStatusException` (pass-through — the framework's own 4xx/5xx kept, 4xx logged at WARN), and `Exception` (500), each rendered into `ErrorResponse`.
* `odd-platform-api/.../controller/DataEntityController.java` — the platform's largest controller: 40 methods returning `Mono<ResponseEntity<...>>` (34 via `.map(ResponseEntity::ok)`, 3 via `.thenReturn(ResponseEntity...)`), with no controller-level mapping annotations and no `@ExceptionHandler`.
* `odd-platform-api/.../controller/SearchController.java` — 7 methods, all `Mono<ResponseEntity<...>>` ending in `.map(ResponseEntity::ok)`; no controller-level error handling. `TermController.java` shows the same shape across 23 methods.
* Across the module, every controller returning `Mono<ResponseEntity<...>>` uses the standard `ResponseEntity` builders (`ok` / `noContent` / `status`) — there is no controller carrying its own exception translation.

## See also

* [ADR-0001 — Contract-first HTTP layer](/developer-guides/architecture-decision-log/adr-0001-openapi-generated-controller-interfaces.md) — the controllers implement generated `*Api` interfaces; the uniform `Mono<ResponseEntity<T>>` return type is the shape those generated signatures take.
* [ADR-0002 — Centralised path-matcher authorization](/developer-guides/architecture-decision-log/adr-0002-centralised-path-matcher-authorization.md) — the companion "one place" decision for authorization; together they keep controllers as thin delegates.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0007-uniform-reactive-controller-pipeline.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
