Files
clean-architecture-backend-…/docs/adr/ADR-GRPC-002-schema-governance-without-protoc.md
T

3.5 KiB

ADR-GRPC-002: Schema governance runs without protoc and without the Buf CLI

  • Status: accepted
  • Date: 2026-08-30
  • Scope: :grpc:grpc-proto-contract, :grpc:grpc-codegen

Context

Stable Tasks 8 through 11 require proto style rules, Buf format/lint/breaking governance, a single Java codegen owner, and a descriptor artifact whose consumer-compile result gates a release.

Two of the tools those tasks name are absent from this toolchain. The Buf CLI is not installed. And protoc is available through the Gradle protobuf plugin, but every leaf in this repository passes spotless with google-java-format, checkstyle, SpotBugs at HIGH confidence, Error Prone and -Werror — and generated protobuf sources pass none of them. Turning codegen on means excluding a source set from five quality gates.

There is precedent for such an exclusion: the jmh source set has spotbugsJmh and checkstyleJmh disabled and Error Prone off. So the carve-out is available. It is also a decision about the quality baseline of a leaf, taken for one task, and outside what this work was asked to change.

adapter:inbound:grpc also carries a recorded decision in the opposite direction: its CLAUDE.md forbids the protobuf plugin and .proto in that leaf, on the grounds that a consuming feature module should own its schema.

Decision

Commit the .proto sources and implement every rule the tasks require as executable Java, with no protoc run and no Buf CLI invocation.

GrpcProtoContractValidator reads .proto text and enforces proto3 syntax, the {organization}.{domain}.v{major} package rule, java_multiple_files, a generated Java package disjoint from the hand-written one, _UNSPECIFIED enum zero values, reserved declarations checked against a supplied removal history, a well-known-type allowlist and a map-field allowlist. It runs against the committed schema in its own test, so the shipped .proto files are live rather than decorative.

GrpcBufPolicy fixes the breaking gate at Buf's FILE category and names the four lifecycle stages a compliant pipeline registers. GrpcCodegenManifest fixes one codegen owner and refuses a literal generator version. GrpcDescriptorArtifact, GrpcConsumerFixture and GrpcSchemaArtifactPublisher carry the schema hash, the descriptor digest and the per-consumer source-break report, and refuse a publish that breaks a consumer or republishes a released version with different bytes.

The committed buf.yaml states the same rules, so running the CLI in an environment that has it reaches the same verdict.

Consequences

The invariants are enforced; the process is not run. Everything Tasks 8 to 11 are about — which schema changes are refused, which consumer breaks block a release, who owns generation — is a build-checkable rule here. What is missing is the protoc invocation and the Buf binary.

Turning codegen on is a bounded change. GrpcCodegenManifest.caSkeleton() already names the owner, the managed version source, the build-directory output paths and the disjoint package policy that a real plugin configuration has to satisfy. The work is a source-set carve-out and a plugin block, not a redesign.

The fixtures use a text codec. GrpcTextCodec gives the testkit a UTF-8 marshaller so the in-process and Netty lanes can exercise interceptors, status mapping, metadata limits and stream sequencing without generated stubs. Those contracts are properties of the platform and the transport, not of any message shape, so the substitution costs nothing — and the lanes run today rather than after codegen lands.