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

4.0 KiB

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.