ADR-0003 — gRPC internally, REST externally, protobuf as single contract
- Status: Accepted
- Date: 2026-06-11
- Related: 01 R8, ADR-0002, ADR-0015
Context
The brief mandates gRPC internally and REST externally. That is a sound split — gRPC gives typed, efficient, streaming inter-service contracts; REST/JSON is what browsers, third parties, and casual clients expect. The risk is maintaining two API definitions for the same operations, which drift (R8). Even in v1 (modular monolith) we want module APIs defined as gRPC so extraction is seamless (ADR-0001).
Decision
- Protobuf is the single source of truth for all service contracts and domain
events, living in
proto/(ADR-0002). - Internal: modules/services expose gRPC APIs (typed, streaming where useful — e.g. sync). In v1 these are in-process gRPC or direct interface calls; the contract shape is identical post-extraction.
- External: a REST/JSON API at the gateway, generated/mapped from the same protos (grpc-gateway annotations or a thin hand-written BFF for ergonomics). OpenAPI is generated for clients and docs.
- Codegen (buf) produces Go stubs, the TS web client, and the OpenAPI spec from the same protos.
Consequences
Positive
- One schema, two transports → no drift (R8); the REST API and Go/TS clients are generated, not hand-synced.
- gRPC contracts from day one make module extraction a deployment change (ADR-0001).
- Streaming RPCs available for sync/change feeds.
Negative / costs
- Codegen toolchain (buf + plugins) to set up and maintain.
- grpc-gateway mappings are sometimes awkward for very REST-idiomatic shapes; where it fights us, we hand-write thin BFF handlers over the gRPC client (still one contract underneath).
- A learning curve for contributors unfamiliar with protobuf/buf.
Alternatives considered
- REST-only everywhere: rejected — loses typed internal contracts and the portfolio gRPC/streaming demonstration; hand-written clients drift.
- gRPC-Web to the browser directly: rejected for the public API — REST/JSON is the lingua franca for third parties and simpler to consume; gRPC-Web stays an option for first-party web internals if needed.
- GraphQL gateway: rejected — adds a third schema language and resolver layer; not justified by current needs (revisit only if client over/under-fetching becomes a real problem).