> 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-0072-contract-first-reactive-stack.md).

# ADR-0072: The platform is a contract-first, reactive, two-language stack

## Status

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

## Context

The platform is a long-lived server with a rich browser UI and a high-fan-in ingestion surface. Two cross-cutting choices shape almost every file: how the server handles concurrency (a thread per request, or a reactive event loop), and how the Java server and the TypeScript browser agree on the shape of the API between them. Left implicit, both get re-decided per feature and drift — a blocking call slips into a reactive chain, or a UI type falls out of sync with the server's DTO and the mismatch is only found at runtime.

## Decision

The platform commits to **three coupled choices that together define the stack**:

* **Reactive end-to-end.** The backend is Spring **WebFlux** (not servlet MVC); data access is **R2DBC** (reactive PostgreSQL, through jOOQ's reactive operations); and the request path is non-blocking from controller to database. Every controller method returns `Mono`/`Flux` — the uniform `Mono<ResponseEntity<T>>` shape of ADR-0007 — and multi-step writes run inside a reactive `@ReactiveTransactional` boundary.
* **Contract-first.** A single OpenAPI document is the source of truth for the HTTP API. The backend **generates** its controller interfaces (`*Api`) from it — controllers are thin implementations (ADR-0001), scoped per resource by OpenAPI tag (ADR-0008) — and the frontend **generates** its API client from the *same* document. Neither side hand-writes the wire types.
* **Two languages, one bridge.** The server is Java/Spring; the UI is a React 18 + TypeScript single-page app built with Vite. They share no runtime — only the generated contract. The one artefact that must stay in sync is the OpenAPI document, and code generation on both sides keeps each honest to it.

## Consequences

* **The API cannot silently drift between server and client.** A change to the OpenAPI document regenerates both the Java interfaces and the TypeScript client, so a breaking change surfaces as a compile error on whichever side lags — not as a runtime 4xx discovered in the browser.
* **Reactivity is a whole-stack commitment, not a per-endpoint choice.** The payoff is high concurrency on few threads — a good fit for the fan-in ingestion surface and for keeping PostgreSQL the only dependency (ADR-0071). The cost is that *any* blocking call on the request path (a synchronous driver, a blocking library) stalls the event loop, so contributors must keep the whole chain non-blocking and use the reactive idioms throughout.
* **Backend and frontend meet at the contract.** A new endpoint is added to the OpenAPI document first, then implemented on each side; the contract leads, the code follows. This is a discipline as much as a tooling choice.
* **The wire field names are the contract's, not the client's — and the casing is not uniform.** The contract names fields in `snake_case` as the house convention, and each generated client re-exposes them in its own idiom: the TypeScript client camelCases them in its `*FromJSON` converters (`test_results` → `testResults`), and the Java models bind them with `@JsonProperty("snake_case")`. Code that goes through a generated client never sees the wire names; code that touches the **raw wire** — a test intercepting an HTTP response, a debugging proxy, a manual `curl` — must use the `snake_case` contract names. The convention has exceptions: eight fields are `camelCase` in the contract itself (`authType`, `projectVersion`, `usedCount`, `hasNext`, `maxSize`, `fileName`, `dataEntityId`, `dataEntityName`), so those reach the wire as written. The casing cannot be normalised without breaking every existing client, so the inconsistency is documented here rather than changed.
* The trade-offs the reactive choice carries at the edges are recorded in its instance ADRs — the uniform pipeline that hides per-endpoint error mapping (ADR-0007), and the transaction-boundary asymmetry where list-shaped reads stay outside a transaction while multi-step writes are wrapped in one.

## Evidence

* `gradle/libs.versions.toml:45` (`spring-boot-starter-webflux`, bundled at `:120`) + `odd-platform-api/build.gradle:25` (`libs.bundles.r2dbc`) + `gradle/libs.versions.toml:72` (`r2dbc-postgresql`) — the backend is reactive WebFlux over reactive Postgres; there is no servlet (`spring-boot-starter-web`) stack on the dependency set.
* `odd-platform-api/.../controller/IngestionController.java:38` (representative of every controller) — methods return `Mono<ResponseEntity<...>>`; reactive types flow down to the repository layer via jOOQ reactive operations (ADR-0007).
* `odd-platform-api/.../service/DataEntityServiceImpl.java:197` — `@ReactiveTransactional` marks the reactive transaction boundary for multi-step writes (≈40 usages across the service layer).
* `odd-platform-api-contract/build.gradle:9-13,44` — the backend generates its API code from `odd-platform-specification/openapi.yaml` (`openApiGenerate { inputSpec = "…/openapi.yaml" }`; `compileJava.dependsOn tasks.openApiGenerate`).
* `odd-platform-ui/openapi-config.yaml` — the frontend generates a `typescript-fetch` client from the **same** `odd-platform-specification/openapi.yaml` into `odd-platform-ui/src/generated-sources`; `odd-platform-ui/package.json` — React 18 + TypeScript 5 + Vite.
* `odd-platform-specification/components.yaml` — field names are `snake_case` by convention (≈195; e.g. `test_results`, `page_info`, `entity_classes`) with eight `camelCase` exceptions (`usedCount`, `hasNext`, `dataEntityName`, `dataEntityId`, `maxSize`, `fileName`, `projectVersion`, `authType`). The generated TypeScript client camelCases the wire names in its converters (`odd-platform-ui/src/generated-sources/models/DataQualityResults.ts` maps `json['test_results']` → `testResults`); the Java models bind the wire names with `@JsonProperty("…")` (snake-named bindings such as `access_token`, `entity_classes`, `external_name`).

## See also

* [ADR-0001 — Contract-first HTTP layer](/developer-guides/architecture-decision-log/adr-0001-openapi-generated-controller-interfaces.md) — the backend half of the contract: controllers are thin implementations of generated `*Api` interfaces.
* [ADR-0007 — Uniform reactive controller pipeline](/developer-guides/architecture-decision-log/adr-0007-uniform-reactive-controller-pipeline.md) — the shape every reactive controller method takes.
* [ADR-0008 — OpenAPI tags scope the generated API interfaces](/developer-guides/architecture-decision-log/adr-0008-openapi-tag-per-resource-scoping.md) — how the single contract is partitioned into per-resource interfaces.
* [ADR-0071 — PostgreSQL is the only required runtime dependency](/developer-guides/architecture-decision-log/adr-0071-postgres-only-runtime-dependency.md) — R2DBC gives this reactive stack non-blocking access to that one datastore.
* [ADR-0070 — Ingestion is one wire contract shared by pull and push producers](/developer-guides/architecture-decision-log/adr-0070-pull-push-ingestion-contract.md) — the ingestion API is the other contract-first surface, served by this reactive stack.
