From a334ae7a7efd048eb06c96d4ae61c2f5e796f54d Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Mon, 5 Oct 2026 16:56:30 -0400 Subject: [PATCH 1/7] docs: Add ADR 0039 - Base API standards on IETF RFCs Co-Authored-By: Claude Opus 5.5 --- .../0039-base-api-standards-on-ietf-rfcs.md | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md diff --git a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md new file mode 100644 index 000000000..9932bf684 --- /dev/null +++ b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md @@ -0,0 +1,159 @@ +--- +adr: "0039" +status: Proposed +date: 2026-10-05 +tags: [server, server-sdk] +--- + +# 0039 - Base API standards on IETF RFCs + + + +{/* cspell:ignore ciphertext Hydra */} + +## Notation + +This ADR uses [RFC 2119](https://www.rfc-editor.org/info/rfc2119/) keywords (`MUST`, `MUST NOT`, +`SHOULD`, `SHOULD NOT`, `MAY`) deliberately. + +## Context and problem statement + +[ADR-0036](./0036-service-to-service-api-standards.md) requires service-to-service APIs to conform +to a comprehensive, documented set of standards. It does not say what those standards are built on: +an existing specification, published design guidelines, a different API technology, or nothing at +all. This ADR decides that. The ADRs that follow set the standards themselves: + +- ADR-0040: request, response, and error payloads. +- ADR-0041: contracts and their evolution. +- ADR-0042: creating resources. +- ADR-0043: reading resources. +- ADR-0044: updating and deleting resources. +- ADR-0045: acting upon resources. +- ADR-0046: bulk processing resources. + +Authentication and authorization will follow in a later ADR. + +Three ground rules keep the comparison fair: + +- **Earlier decisions are not criteria.** ADR-0035's OpenAPI requirement and ADR-0036's list of + topics both assume HTTP resource APIs. Choosing GraphQL or gRPC would mean revising them, and that + cost is not counted against those options. +- **No option is adopted wholesale.** Each would be adapted, so the choice is what the standards + start from and borrow from, not whom they defer to. Whatever the starting point, the standards + state every rule themselves. +- **Every audience counts.** The scope is service-to-service APIs, but each option is judged as if + it would also serve our own clients and our customers. A second standard for those APIs would mean + two of everything. + +## Decision drivers + +1. Answers the common design questions: payloads, errors, querying, paging, concurrency, bulk + operations, asynchronous work, versioning, and deprecation. +2. Leaves room for a version, so an unavoidable breaking change has somewhere to go. +3. Customer-friendly: callable from any language, from a browser, and with `curl`. +4. Machine-readable contracts and generated clients in C#, Rust, TypeScript, Swift, and Kotlin. +5. The owning service decides what can be queried. Most vault data is ciphertext, which cannot be + filtered or sorted. +6. Works through customer-operated proxies on self-hosted installs, and with standard HTTP + infrastructure. +7. Fits ASP.NET Core and data access that is largely Dapper over stored procedures. + +## Considered options + +- **Our own standards on IETF RFCs:** write the standards ourselves, and wherever an IETF RFC covers + a topic, adopt it or record why not. +- **[JSON:API](https://jsonapi.org/):** a published specification for JSON resource APIs, covering + document shape, errors, and query parameter families. +- **Hypermedia formats:** [HAL](https://datatracker.ietf.org/doc/draft-kelly-json-hal/), + [Siren](https://github.com/kevinswiber/siren), + [Collection+JSON](https://github.com/collection-json/spec), and + [JSON-LD](https://www.w3.org/TR/json-ld11/) with + [Hydra](https://www.hydra-cg.com/spec/latest/core/). Links and actions let a client discover an + API at run time. +- **[OData](https://www.odata.org/documentation/):** an OASIS standard with a URL query language + over a declared entity data model. +- **[GraphQL](https://spec.graphql.org/):** a typed schema and caller-composed queries, usually sent + to a single endpoint. +- **[gRPC](https://grpc.io/):** Protocol Buffers contracts called over HTTP/2 with binary payloads. +- **[Google AIP](https://google.aip.dev/):** Google's numbered API design guidance, defined in + Protocol Buffers. +- **[Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md):** + Microsoft's guidelines for Azure HTTP APIs. + +✅ yes, ❌ no, ⚠️ partly or with qualification. + +| | Own on RFCs | JSON:API | Hypermedia | OData | GraphQL | gRPC | AIP | Azure | +| ----------------------------------- | :---------: | :------: | :--------: | :---: | :-----: | :--: | :-: | :---: | +| Answers the common design questions | ⚠️ | ⚠️ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | +| Room for a version | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | +| Contracts express required fields | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ✅ | +| Customer-friendly | ✅ | ✅ | ✅ | ⚠️ | ❌ | ❌ | ⚠️ | ✅ | +| Generated clients | ✅ | ✅ | ⚠️ | ⚠️ | ✅ | ✅ | ✅ | ✅ | +| Owner decides what can be queried | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | +| Works through customer proxies | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ⚠️ | ✅ | +| Works with HTTP infrastructure | ✅ | ✅ | ✅ | ✅ | ❌ | ⚠️ | ⚠️ | ✅ | +| Fits our stack | ✅ | ⚠️ | ✅ | ⚠️ | ✅ | ✅ | ⚠️ | ✅ | +| Streaming and push | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ⚠️ | ❌ | +| Compact wire format | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ⚠️ | ❌ | +| Published origin | ⚠️ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | + +## Decision outcome + +Chosen option: **Our own standards on IETF RFCs**. + +- Three options are customer-friendly and leave the query surface entirely to the owning service: + this one, JSON:API, and the hypermedia formats. The hypermedia formats answer almost none of the + design questions. JSON:API answers more, but would be departed from in many places. It does not + cover concurrency or deprecation, and covers asynchronous processing only in non-normative + recommendations. This option takes RFCs topic by topic and covers those topics through them. +- Where a published standard exists, it is used as published. Where none exists, the standards + supply their own answer. They are therefore entirely ours without being invented from nothing. +- Good conventions from elsewhere remain available on their merits, with credit, such as JSON:API's + document shape and query parameters and AIP-136's custom-method syntax. + +The rules: + +1. The standards `MUST` be self-contained. Every rule is stated in the standards, and no rule is + incorporated by reference. +2. Where an IETF RFC covers a topic, the standards `SHOULD` adopt it as published. Where they keep + their own answer instead, they `MUST` record why. +3. Conventions taken from other specifications `SHOULD` be credited to their source. + +### Positive consequences + +- One set of answers, stated in our own words, with every borrowed rule credited. +- Wherever an RFC exists, a rule rests on an IETF standard that HTTP libraries and tooling already + understand. +- The owning service keeps control of what can be queried. +- The same standards could later serve client-facing and public APIs without a second design. + +### Negative consequences + +- Topics with no RFC, such as payload shape, filtering, sorting, paging, and bulk operations, are + ours to write and maintain. +- No single outside document describes the standards. They are learned from these ADRs. +- Every departure from an RFC needs a recorded reason, and each is a point someone may contest. + +### Rejected options + +- **JSON:API:** full conformance is impractical, because its media type, `PATCH`-only updates, `400` + for unrecognized query parameters, and `relationships` objects would all be departed from. It does + not cover concurrency or deprecation, and covers asynchronous processing only in non-normative + recommendations. Its useful conventions remain available to the standards, with credit. +- **Hypermedia formats:** they answer almost none of the design questions, and are built for clients + that discover an API at run time, where ours are generated at build time. JSON-LD aside, none is + an adopted standard. +- **OData:** a general-purpose query language over every exposed property, most of which cannot + apply to ciphertext. `$expand` does not cross service boundaries, its ASP.NET Core library depends + on `IQueryable`, and `$filter` is a string grammar. +- **GraphQL:** it discourages versioning, cannot be used without learning a query language, needs + authorization per field and per path plus GraphQL-aware infrastructure, and leaves write semantics + and most design questions to us. +- **gRPC:** a transport rather than an API design standard. proto3 has no required fields, customers + and browsers need additional tooling, and HTTP/2 with trailers cannot be guaranteed through + customer-operated proxies. +- **Google AIP:** its tooling is protobuf-first, its filter grammar is a string language, its errors + are Google-specific, and it is written for Google's infrastructure. +- **Azure guidelines:** written for Azure service teams and infrastructure, with an Azure-specific + error envelope, a dated `api-version` query parameter on every request, and an OData-style filter + grammar. From c08cf038321017f0353ad03b649bd725995f0830 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 6 Oct 2026 06:30:16 -0400 Subject: [PATCH 2/7] Clarify that GraphQL and gRPC remain available as additional interfaces Co-Authored-By: Claude Opus 5.5 --- .../adr/0039-base-api-standards-on-ietf-rfcs.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md index 9932bf684..d92805795 100644 --- a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md +++ b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md @@ -119,6 +119,11 @@ The rules: their own answer instead, they `MUST` record why. 3. Conventions taken from other specifications `SHOULD` be credited to their source. +This decision does not preclude GraphQL or gRPC. A service `MAY` also offer either where it makes +sense, for example GraphQL where callers need to compose data from a richly connected model, or gRPC +on a high-volume path between services. The standards govern the HTTP APIs that services publish, +and a GraphQL or gRPC interface is offered in addition to those, not instead of them. + ### Positive consequences - One set of answers, stated in our own words, with every borrowed rule credited. @@ -136,6 +141,9 @@ The rules: ### Rejected options +Each of these was rejected as the foundation for the standards. As noted above, GraphQL and gRPC +remain available as additional interfaces. + - **JSON:API:** full conformance is impractical, because its media type, `PATCH`-only updates, `400` for unrecognized query parameters, and `relationships` objects would all be departed from. It does not cover concurrency or deprecation, and covers asynchronous processing only in non-normative From 86e4db5d14af4ca75ab1ea8c95522539b5355836 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 6 Oct 2026 08:56:53 -0400 Subject: [PATCH 3/7] Explain each row of the comparison table Co-Authored-By: Claude Opus 5.5 --- .../0039-base-api-standards-on-ietf-rfcs.md | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md index d92805795..ec5fe5502 100644 --- a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md +++ b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md @@ -9,7 +9,7 @@ tags: [server, server-sdk] -{/* cspell:ignore ciphertext Hydra */} +{/* cspell:ignore ciphertext CSDL Hydra */} ## Notation @@ -97,6 +97,49 @@ Three ground rules keep the comparison fair: | Compact wire format | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ⚠️ | ❌ | | Published origin | ⚠️ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | +What each row means: + +- **Answers the common design questions:** whether the option itself answers the questions every API + faces, such as payload shape, errors, querying, paging, concurrency, bulk operations, asynchronous + work, versioning, and deprecation, rather than leaving them to us. ✅ means most are answered, ⚠️ + some, ❌ few. +- **Room for a version:** whether an API can carry a version and introduce a new one when a breaking + change is unavoidable. GraphQL is ⚠️ because, although versioning is possible, its documentation + discourages it in favor of additive evolution. +- **Contracts express required fields:** whether a contract can say that a field is always present, + so contracts do not drift toward every field being optional. proto3 has no required fields, so + gRPC and AIP can express this only through validation annotations. +- **Customer-friendly:** whether a developer who has never seen Bitwarden can call the API from any + language, from a browser, and with `curl`, without learning a query language or installing special + tooling. +- **Generated clients:** whether a machine-readable contract (OpenAPI, a GraphQL schema, or Protocol + Buffers) can generate typed clients in the languages we ship: C#, Rust, TypeScript, Swift, and + Kotlin. A JSON:API API is described in OpenAPI like any other HTTP API, so standard generators + work. Its envelope appears in the generated types unless the generators are configured to hide it, + which is a question of payload shape, not of whether clients can be generated. Hypermedia formats + are ⚠️ because their links add little to a generated client, and OData because its contract is + CSDL, with OpenAPI produced by conversion. +- **Owner decides what can be queried:** whether the owning service controls exactly which fields + can be filtered, sorted, or expanded, rather than exposing a general-purpose query language over + every field. This matters because most vault data is ciphertext, and every supported query has to + be indexed and authorized. +- **Works through customer proxies:** whether the API works through the proxies and load balancers + that self-hosted customers operate, which cannot be assumed to support HTTP/2 with trailers end to + end. +- **Works with HTTP infrastructure:** whether caching, rate limiting, firewall rules, metrics, and + tracing can work per route and method without understanding request bodies. GraphQL sends + everything as a `POST` to one endpoint. +- **Fits our stack:** whether the option is well supported on ASP.NET Core and on data access that + is largely Dapper over stored procedures. JSON:API is ⚠️ because its .NET implementations are + oriented to controllers and Entity Framework Core, and OData because its ASP.NET Core library + depends on `IQueryable`. +- **Streaming and push:** whether the option itself supports streaming responses or pushing changes + to callers. +- **Compact wire format:** whether payloads use an efficient binary encoding rather than JSON text. +- **Published origin:** whether the option comes from a published, adopted specification. ⚠️ means + guidelines, drafts, or, for our own standards, an RFC per topic where one exists rather than a + single document. + ## Decision outcome Chosen option: **Our own standards on IETF RFCs**. From d52a403ab6109486f95eb856ecd1354ebc409b03 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 6 Oct 2026 09:42:12 -0400 Subject: [PATCH 4/7] Spell out the reasons each option was rejected Co-Authored-By: Claude Opus 5.5 --- .../0039-base-api-standards-on-ietf-rfcs.md | 110 ++++++++++++++---- 1 file changed, 89 insertions(+), 21 deletions(-) diff --git a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md index ec5fe5502..f2211fb65 100644 --- a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md +++ b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md @@ -187,24 +187,92 @@ and a GraphQL or gRPC interface is offered in addition to those, not instead of Each of these was rejected as the foundation for the standards. As noted above, GraphQL and gRPC remain available as additional interfaces. -- **JSON:API:** full conformance is impractical, because its media type, `PATCH`-only updates, `400` - for unrecognized query parameters, and `relationships` objects would all be departed from. It does - not cover concurrency or deprecation, and covers asynchronous processing only in non-normative - recommendations. Its useful conventions remain available to the standards, with credit. -- **Hypermedia formats:** they answer almost none of the design questions, and are built for clients - that discover an API at run time, where ours are generated at build time. JSON-LD aside, none is - an adopted standard. -- **OData:** a general-purpose query language over every exposed property, most of which cannot - apply to ciphertext. `$expand` does not cross service boundaries, its ASP.NET Core library depends - on `IQueryable`, and `$filter` is a string grammar. -- **GraphQL:** it discourages versioning, cannot be used without learning a query language, needs - authorization per field and per path plus GraphQL-aware infrastructure, and leaves write semantics - and most design questions to us. -- **gRPC:** a transport rather than an API design standard. proto3 has no required fields, customers - and browsers need additional tooling, and HTTP/2 with trailers cannot be guaranteed through - customer-operated proxies. -- **Google AIP:** its tooling is protobuf-first, its filter grammar is a string language, its errors - are Google-specific, and it is written for Google's infrastructure. -- **Azure guidelines:** written for Azure service teams and infrastructure, with an Azure-specific - error envelope, a dated `api-version` query parameter on every request, and an OData-style filter - grammar. +#### JSON:API + +- Full conformance is impractical. Its required media type, `PATCH` as the only way to update, `400` + for any unrecognized query parameter, and `relationships` objects for references to other + resources would all be departed from, leaving a long list of deviations from a specification the + standards would cite. +- It does not cover concurrency or deprecation, and covers asynchronous processing only in + non-normative recommendations, so those topics would be ours to write anyway. +- Existing .NET implementations are oriented to controllers and Entity Framework Core rather than + minimal APIs over Dapper. +- Adopting it as the foundation is not needed to use its best ideas. Its document shape and query + parameter families remain available to the standards on their merits, with credit. + +#### Hypermedia formats + +- They answer almost none of the common design questions. Beyond links, and an error object in + Collection+JSON, errors, validation, paging, filtering, sorting, concurrency, bulk operations, and + long-running operations are all left to us. +- They are built for clients that discover an API at run time. Our clients are generated from a + contract at build time and already know the operations, so links are weight that generated clients + do not use. +- None is an adopted API standard. HAL is an individual IETF draft that expired in 2024 without + being adopted, Siren has not changed since 2020, and JSON-LD is a data format rather than an API + standard. +- Links, their one distinctive feature, can be added to the standards later if a use for them + appears. + +#### OData + +- Its query language reaches every exposed property unless restricted, where the owning service + needs to control exactly which queries it supports, because each one has to be indexed and + authorized. +- Most of that query power cannot apply to zero-knowledge data, because ciphertext cannot be + filtered or sorted. +- `$expand` assumes related entities live in the same model. Across service boundaries, they live in + another service's store. +- Its ASP.NET Core library translates queries to `IQueryable`, which suits Entity Framework but not + Dapper over stored procedures. +- `$filter` is a string expression language, so building one means concatenating, quoting, and + escaping strings, which is how injection bugs happen. +- It does not specify API versioning. The `OData-Version` header versions the protocol, not the API. + +#### GraphQL + +- It discourages versioning. Its documentation "takes a strong opinion on avoiding versioning" in + favor of additive evolution, the posture that eventually makes every new field optional. +- Customers cannot walk up and use it. Calling it means learning schemas, fragments, variables, and + nullability. +- The request surface is any traversal the schema allows, so authorization has to be decided per + field and per path through the graph. +- Every request is a `POST` to one endpoint, so caching, rate limiting, firewall rules, and tracing + need GraphQL-aware infrastructure, along with query depth and cost limits. +- Mutations have no standard semantics, so create, update, delete, idempotency, concurrency, and + most other design questions would still be ours. +- Its main strength, caller-composed queries over a richly connected graph, has little to work with + when most fields are ciphertext and each service owns a narrow slice of the data. + +#### gRPC + +- It is a transport and an interface definition language, not an API design standard. Apart from + status codes, it does not address paging, filtering, sorting, bulk operations, concurrency, or + deprecation, so it would still need design guidance alongside it. +- proto3 has no required fields. Required-ness can be expressed only through validation annotations, + so the contract itself treats every field as optional. +- Customers and browsers cannot call it without extra tooling. Payloads are binary, the command line + needs specialized tools, and browsers need gRPC-Web and a proxy. Serving them through JSON + transcoding adds a second surface to document, test, and secure. +- It needs HTTP/2, including trailers, end to end, which cannot be guaranteed through + customer-operated proxies on self-hosted installs. + +#### Google AIP + +- It is protobuf-first. The definition is the `.proto` file and its linter works on it, so applied + to APIs described any other way it loses its tooling and becomes a style guide. +- Its filter grammar (AIP-160) is a string expression language, with the same drawbacks as OData's. +- Errors use `google.rpc.Status`, a Google-specific shape. +- It is written for Google's own APIs, and many proposals assume Google's infrastructure, so we + would adopt only a subset. + +#### Microsoft Azure REST API Guidelines + +- They are written for Azure service teams and enforced by Microsoft's own review process, and many + rules assume Azure infrastructure, such as Resource Manager, Azure SDK conventions, and `x-ms-` + headers. +- The error envelope is Azure's own. +- Versioning is a dated `api-version` query parameter on every request, built around Azure's release + process. +- Filter expressions follow OData syntax, a string grammar. +- They are guidelines rather than a specification, revised at Microsoft's discretion. From 75d8694d51a165e031ef8bfabb93f13a02e7322d Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 6 Oct 2026 09:45:34 -0400 Subject: [PATCH 5/7] Drop the ciphertext argument, which applies to few APIs Co-Authored-By: Claude Opus 5.5 --- .../adr/0039-base-api-standards-on-ietf-rfcs.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md index f2211fb65..c82e6719a 100644 --- a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md +++ b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md @@ -9,7 +9,7 @@ tags: [server, server-sdk] -{/* cspell:ignore ciphertext CSDL Hydra */} +{/* cspell:ignore CSDL Hydra */} ## Notation @@ -52,8 +52,8 @@ Three ground rules keep the comparison fair: 2. Leaves room for a version, so an unavoidable breaking change has somewhere to go. 3. Customer-friendly: callable from any language, from a browser, and with `curl`. 4. Machine-readable contracts and generated clients in C#, Rust, TypeScript, Swift, and Kotlin. -5. The owning service decides what can be queried. Most vault data is ciphertext, which cannot be - filtered or sorted. +5. The owning service decides what can be queried, because every query it supports has to be indexed + and authorized. 6. Works through customer-operated proxies on self-hosted installs, and with standard HTTP infrastructure. 7. Fits ASP.NET Core and data access that is largely Dapper over stored procedures. @@ -121,8 +121,7 @@ What each row means: CSDL, with OpenAPI produced by conversion. - **Owner decides what can be queried:** whether the owning service controls exactly which fields can be filtered, sorted, or expanded, rather than exposing a general-purpose query language over - every field. This matters because most vault data is ciphertext, and every supported query has to - be indexed and authorized. + every field. This matters because every supported query has to be indexed and authorized. - **Works through customer proxies:** whether the API works through the proxies and load balancers that self-hosted customers operate, which cannot be assumed to support HTTP/2 with trailers end to end. @@ -219,8 +218,7 @@ remain available as additional interfaces. - Its query language reaches every exposed property unless restricted, where the owning service needs to control exactly which queries it supports, because each one has to be indexed and authorized. -- Most of that query power cannot apply to zero-knowledge data, because ciphertext cannot be - filtered or sorted. + - `$expand` assumes related entities live in the same model. Across service boundaries, they live in another service's store. - Its ASP.NET Core library translates queries to `IQueryable`, which suits Entity Framework but not @@ -241,8 +239,8 @@ remain available as additional interfaces. need GraphQL-aware infrastructure, along with query depth and cost limits. - Mutations have no standard semantics, so create, update, delete, idempotency, concurrency, and most other design questions would still be ours. -- Its main strength, caller-composed queries over a richly connected graph, has little to work with - when most fields are ciphertext and each service owns a narrow slice of the data. +- Its main strength, caller-composed queries over a richly connected graph, has less to work with + when each service owns a narrow slice of the data. #### gRPC From d6c359cfdb97e51392fd5e9fde2a7981b94217f7 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 6 Oct 2026 09:48:45 -0400 Subject: [PATCH 6/7] Drop the OData query-surface reason Co-Authored-By: Claude Opus 5.5 --- docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md index c82e6719a..34e278632 100644 --- a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md +++ b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md @@ -215,10 +215,6 @@ remain available as additional interfaces. #### OData -- Its query language reaches every exposed property unless restricted, where the owning service - needs to control exactly which queries it supports, because each one has to be indexed and - authorized. - - `$expand` assumes related entities live in the same model. Across service boundaries, they live in another service's store. - Its ASP.NET Core library translates queries to `IQueryable`, which suits Entity Framework but not From ccb2015b7c224c1b5220ac2f66cb2dc6f170b174 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 6 Oct 2026 09:51:43 -0400 Subject: [PATCH 7/7] Add four reasons for rejecting OData Co-Authored-By: Claude Opus 5.5 --- .../adr/0039-base-api-standards-on-ietf-rfcs.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md index 34e278632..9e74bd8c9 100644 --- a/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md +++ b/docs/architecture/adr/0039-base-api-standards-on-ietf-rfcs.md @@ -215,8 +215,18 @@ remain available as additional interfaces. #### OData +- It exposes the data model rather than the domain. Queries are built from entities, properties, and + relationships, so callers couple themselves to a service's internal structure, and a structural + change becomes a breaking change. +- It is a full application protocol, not a set of conventions. Beyond querying, it brings `$apply`, + `$compute`, `$search`, `$batch`, and more, so the standards would have to decide which parts are + required, optional, or prohibited. - `$expand` assumes related entities live in the same model. Across service boundaries, they live in another service's store. +- It is not walk-up usable. Callers have to learn its query language and conventions before they can + use it. +- It solves a problem most of our APIs do not have. Rich, caller-driven querying is valuable for + data-oriented APIs, but it is more than most APIs need from their foundation. - Its ASP.NET Core library translates queries to `IQueryable`, which suits Entity Framework but not Dapper over stored procedures. - `$filter` is a string expression language, so building one means concatenating, quoting, and