Files
clean-architecture-backend-…/docs/adr/ADR-GQL-001-graphql-context-and-storage-ownership.md
T

71 lines
4.0 KiB
Markdown

# ADR-GQL-001 — GraphQL context stays inbound; object authorization moves to application-core; the persisted-operation store stays an inbound SPI
- Status: Accepted
- Date: 2026-08-24
- Review: `docs/reviews/2026-08-14-graphql-module-code-review.md` GQL-026
## Context
The GraphQL leaf's own documentation described three things crossing its boundary: a
`GraphQlRequestContext` with a deadline propagated into application, JPA, Mongo and the HTTP client;
object authorization decided inside the transport; and a persisted-operation registry implemented by
an external durable store.
Two of those invert the dependency direction. If `application-core` or an outbound adapter
implements a type that lives in `adapter:inbound:graphql`, the registry edge that says inbound
depends on application is satisfied while the real compile-time dependency runs the other way.
The third is a business rule in the wrong layer: whether an actor may see an object is a decision
about the domain, and GraphQL is one of four transports this skeleton ships.
## Decision
Three different answers, because the three problems are not the same problem.
**GraphQL context stays inbound-local.** It is mapped explicitly onto application command fields —
actor, tenant, deadline — rather than travelling as a type. Nothing outside the leaf references
`GraphQlRequestContext`, and the boundary test is that grep returns nothing outside it.
**Object authorization moves to `application-core`.** `ObjectAccessPolicy`, `ObjectAccessRequest`
and `ObjectAccessDecision` are transport-neutral and live with the other application policies;
`ApplicationObjectAuthorization` in the GraphQL leaf is the bridge that calls them. This is the one
of the three that was a real layering defect, and it is fixed rather than documented.
**The persisted-operation store stays an inbound-owned SPI.** `GraphQlPersistedOperationRegistry`
remains in `advanced/persisted`, and no leaf outside GraphQL implements it.
## Consequences
The third decision is the one that needs defending, because it leaves the reported risk in place
rather than removing it.
The risk is conditional: the direction inverts only when something outside the leaf implements the
interface. Nothing does. The template ships an in-memory registry and no durable one, because it
ships no persisted-operation store at all.
The alternative was to introduce a generic operational key-value store port owned by a neutral
contract holder, with the GraphQL adapter owning only the key and value mapping. That port would
have exactly one interface, zero implementations and one speculative consumer — a new abstraction
whose shape is guessed from a requirement nobody has stated. This repository has spent a full
remediation pass deleting controls that existed and were reached by nothing, and inventing a port
for a store that does not exist is how the next one of those gets written.
So the decision is to leave the SPI where it is and to move it when a durable store is actually
built. Moving it then is a rename across one leaf and one new adapter, which is cheaper than
carrying a wrong abstraction until then. What must not happen in the meantime is an outbound leaf
implementing the inbound interface, because that is the moment the direction actually inverts, and
it would happen in a commit whose diff looks like an implementation rather than a layering change.
The composition root wires these and owns no business or storage policy of its own.
## Enforcement
`verifyCleanArchitectureDependencies` and `modules.json` hold the leaf's edges to
`domain-core`, `application-core` and `shared-contract`. `ObjectAccessPolicyTest` covers the
application-side policy and `ApplicationObjectAuthorizationTest` the bridge.
The condition this ADR turns on — that nothing outside the GraphQL leaf implements the
persisted-operation SPI — is a claim about the whole repository, so it is checked at the
composition root rather than inside the leaf, next to the other GraphQL boundary rules in
`app-bootstrap`'s architecture suite.