> 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.


---

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