ADR-0001: Contract-first HTTP layer

ODD Platform's REST controllers are thin delegates over OpenAPI-generated interfaces — the HTTP contract lives in the spec, and routes change by regenerating it, not by editing controllers.

Status

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

Context

ODD Platform exposes a large REST surface (hundreds of endpoints across data entities, ingestion, search, lineage, RBAC, collaboration, and more). A platform that size needs one answer to a structural question: where does the HTTP contract live, and how is it kept consistent between the server that implements it and the clients (the web UI, collectors, push adapters) that call it?

There are two broad options: hand-write Spring MVC/WebFlux mappings on each controller, or define the contract once in a specification and generate the wiring from it. Hand-written mappings drift — the spec and the code diverge silently, and each client re-derives the contract by hand. ODD chose the contract-first path.

Decision

Every REST controller in odd-platform-api is a thin delegate that implements an OpenAPI-generated *Api interface. The HTTP method, path, and produces/consumes annotations live on the generated interface — compiled from openapi.yaml into the org.opendatadiscovery.oddplatform.api.contract.api.* package by the odd-platform-api-contract module's openApiGenerate build step — never on the controller class itself.

A controller class carries only @RestController and @RequiredArgsConstructor; each method carries only @Override and delegates straight to a service. Consequently, adding or changing an endpoint is a specification-edit-and-regenerate flow, not a controller edit.

Consequences

  • openapi.yaml (in odd-platform-specification) is the single source of truth for the HTTP surface. The UI's generated TypeScript client and the server's generated interfaces stay in sync by construction.

  • To add an endpoint: edit openapi.yaml, regenerate, then implement the @Override. Putting a @PostMapping / @GetMapping directly on a controller class does not follow the convention — it would create a route that lives outside the contract and silently drift the spec from the code.

  • The generated *Api interfaces are build artifacts and are not committed to the source tree; to read the live contract, read openapi.yaml.

  • Two deliberate exceptions, both external-webhook receivers. AlertManagerController is fully hand-coded — it bridges an external Alertmanager webhook payload that is not part of ODD's generated contract. EventApiController is also fully hand-coded — it receives the Slack events webhook via a direct @PostMapping("/api/slack/events"), again with no generated interface. Both are a single @RestController with a direct @PostMapping because each bridges an external system's webhook payload that is not part of ODD's contract — the deliberate exceptions that prove the rule.

Evidence

  • odd-platform-api/.../controller/AlertController.java:11-17@RestController @RequiredArgsConstructor public class AlertController implements AlertApi; the class declares no @RequestMapping, and its methods are @Override with no mapping annotations.

  • The interface is imported from org.opendatadiscovery.oddplatform.api.contract.api.AlertApi — generated by the odd-platform-api-contract module's openApiGenerate task at build time and absent from the source tree.

  • odd-platform-specification/openapi.yaml — the contract the *Api interfaces are generated from.

  • The same shape holds across the module's controllers (data-entity, ingestion, search, and RBAC controllers all implements their generated *Api with no controller-level mapping annotations). The two deliberate exceptions are the external-webhook receivers, both fully hand-coded with no generated *Api: AlertManagerController.java:15-21 (@RestController, no implements, @PostMapping("ingestion/alert/alertmanager") with a // TODO: define OpenAPI spec note) and EventApiController.java:14-22 (@RestController, no implements, @PostMapping("/api/slack/events") — the Slack events webhook).

See also

Last updated