gRPC services
gRPC uses Protocol Buffers over HTTP/2 for fast, strongly-typed service-to-service communication with generated clients in every language. It shines for internal microservice APIs where performance and contract strictness matter; the design work is the proto contract, its evolution, and using the streaming and deadline features correctly.
Method
- Design the proto as the contract, carefully. The
.protofile defines the service, methods, and message types, and generates clients and servers in every language (see openapi-contracts' spec-first ethic, gRPC edition): so the proto is the API. Design messages for the domain, group related RPCs into services, and treat the proto with the same review rigor as any public contract. - Evolve protos with the compatibility rules. Field numbers are the wire identity and are permanent: never reuse or change a field's number; add new fields with new numbers (old clients ignore them), reserve removed field numbers, and never change a field's type (see schema-evolution, api-change-management: Protobuf's rules are strict and mechanical). Following them gives backward and forward compatibility; breaking them corrupts data silently.
- Choose the right RPC pattern. Unary (request-response, the default), server streaming (one request, stream of responses: feeds, large result sets), client streaming (stream up, one response: uploads, batched ingestion), bidirectional streaming (chat, real-time sync). Pick the pattern matching the interaction; forcing everything into unary loses gRPC's streaming advantage where it fits.
- Propagate deadlines through the call chain. gRPC deadlines flow across service boundaries: a caller's deadline is visible to the callee, which passes it further (see timeouts-and-retries' budget propagation). Set deadlines on every call (never infinite), and honor the incoming deadline (stop work the caller has abandoned): this is how gRPC systems avoid the cascading-timeout failures of naive chains.
- Handle errors with status codes and details. gRPC's status codes (NOT_FOUND, INVALID_ARGUMENT, DEADLINE_EXCEEDED, UNAVAILABLE) plus rich error details (structured error messages): map failures to the right code so clients branch correctly and retry the retryable (UNAVAILABLE yes, INVALID_ARGUMENT no: see api-error-responses, timeouts-and-retries). Use the standard codes' semantics, do not invent your own meanings.
- Add the cross-cutting concerns via interceptors. Auth, logging, tracing, retries, and metrics as interceptors (the middleware equivalent: see the mesh discussion in service-mesh-tradeoffs): applied uniformly across services without per-method code. Deadline propagation, retries with backoff, and load balancing are gRPC-native or mesh-provided; use them rather than reimplementing.
Boundaries
- gRPC excels for internal service-to-service APIs (performance, strict contracts, streaming, polyglot); it is a poor fit for browser clients (needs gRPC-Web and a proxy), public APIs (REST/GraphQL are more accessible), and human-debuggable endpoints (binary, not curl-able). Choose per boundary (see rest-endpoint-design, graphql-schema-design).
- The binary protocol and codegen add tooling weight (proto compilation, generated code in the build); worth it for many services, overhead for a couple. Match to the system's scale.
- Field-number permanence and proto evolution rules are unforgiving; a mistake (reused field number) corrupts data across versions. This strictness is a feature (guaranteed compatibility) that demands discipline.